From cd72b7a94883934fd647bba9a77bf58e64103fff Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 05:20:28 +0800 Subject: [PATCH 1/2] refactor: convert to bun workspaces monorepo - Move web app into apps/web/ - Create packages/shared/ with shared types - Create plugins/decky-vault/ scaffold - Root package.json manages workspaces only --- .gitignore | 71 +++++------ .env.example => apps/web/.env.example | 0 CHANGELOG.md => apps/web/CHANGELOG.md | 0 .../web/__tests__}/proxy.test.ts | 0 .../web/app}/(auth)/forgot-password/page.tsx | 0 {app => apps/web/app}/(auth)/layout.tsx | 0 {app => apps/web/app}/(auth)/login/page.tsx | 0 .../web/app}/(auth)/reset-password/page.tsx | 0 {app => apps/web/app}/(auth)/signup/page.tsx | 0 {app => apps/web/app}/(manage)/layout.tsx | 0 .../app}/(manage)/manage/analytics/page.tsx | 0 .../manage/benchmarks/benchmarks-client.tsx | 0 .../app}/(manage)/manage/benchmarks/page.tsx | 0 .../manage/comments/comments-client.tsx | 0 .../app}/(manage)/manage/comments/page.tsx | 0 .../(manage)/manage/games/games-client.tsx | 0 .../web/app}/(manage)/manage/games/page.tsx | 0 .../manage/hardware/hardware-client.tsx | 0 .../app}/(manage)/manage/hardware/page.tsx | 0 .../web/app}/(manage)/manage/page.tsx | 0 .../web/app}/(manage)/manage/reports/page.tsx | 0 .../manage/reports/reports-client.tsx | 0 .../web/app}/(manage)/manage/storage/page.tsx | 0 .../manage/storage/storage-client.tsx | 0 .../app}/(manage)/manage/suggestions/page.tsx | 0 .../manage/suggestions/suggestions-client.tsx | 0 .../web/app}/(manage)/manage/users/page.tsx | 0 .../(manage)/manage/users/users-client.tsx | 0 .../web/app}/__tests__/metadata.test.ts | 0 .../__tests__/sitemap.integration.test.ts | 0 .../web/app}/__tests__/sitemap.test.ts | 0 .../web/app}/api/[[...slugs]]/route.ts | 0 {app => apps/web/app}/app/[steamid]/page.tsx | 0 .../web/app}/app/__tests__/redirect.test.ts | 0 {app => apps/web/app}/compare/layout.tsx | 0 {app => apps/web/app}/compare/page.tsx | 0 .../web/app}/components/PlatformIcons.tsx | 0 {app => apps/web/app}/contact/layout.tsx | 0 {app => apps/web/app}/contact/page.tsx | 0 .../devices/[slug]/device-detail-client.tsx | 0 .../app}/devices/[slug]/opengraph-image.tsx | 0 {app => apps/web/app}/devices/[slug]/page.tsx | 0 {app => apps/web/app}/devices/page-client.tsx | 0 {app => apps/web/app}/devices/page.tsx | 0 {app => apps/web/app}/game/[id]/edit/page.tsx | 0 .../web/app}/game/[id]/game-page-client.tsx | 0 .../web/app}/game/[id]/opengraph-image.tsx | 0 {app => apps/web/app}/game/[id]/page.tsx | 0 .../app}/game/[id]/preset-detail-modal.tsx | 0 .../web/app}/game/[id]/submit/page.tsx | 0 {app => apps/web/app}/game/add/page.tsx | 0 .../web/app}/games/games-page-client.tsx | 0 {app => apps/web/app}/games/page.tsx | 0 {app => apps/web/app}/globals.css | 0 {app => apps/web/app}/icon.png | Bin {app => apps/web/app}/layout.tsx | 0 {app => apps/web/app}/manifest.ts | 0 {app => apps/web/app}/not-found.tsx | 0 {app => apps/web/app}/opengraph-image.tsx | 0 {app => apps/web/app}/page.tsx | 0 {app => apps/web/app}/profile/[id]/page.tsx | 0 .../app}/profile/[id]/profile-page-client.tsx | 0 {app => apps/web/app}/profile/layout.tsx | 0 {app => apps/web/app}/profile/page.tsx | 0 {app => apps/web/app}/robots.ts | 0 {app => apps/web/app}/search/layout.tsx | 0 {app => apps/web/app}/search/page.tsx | 0 {app => apps/web/app}/sitemap.ts | 0 {app => apps/web/app}/sw.ts | 0 .../web/app}/test-version-fetchers/page.tsx | 0 {app => apps/web/app}/twitter-image.tsx | 0 {app => apps/web/app}/updates/[slug]/page.tsx | 0 {app => apps/web/app}/updates/page.tsx | 0 .../web/components}/anti-cheat-badge.tsx | 0 .../auth/__tests__/placeholder.test.tsx | 0 .../components}/auth/forgot-password-form.tsx | 0 .../web/components}/auth/login-form.tsx | 0 .../web/components}/auth/otp-input.tsx | 0 .../auth/otp-verification-step.tsx | 0 .../components}/auth/passkey-setup-step.tsx | 0 .../components}/auth/password-strength.tsx | 0 .../components}/auth/reset-password-form.tsx | 0 .../web/components}/auth/signup-form-step.tsx | 0 .../web/components}/auth/signup-wizard.tsx | 0 .../web/components}/auth/social-buttons.tsx | 0 .../web/components}/auth/step-indicator.tsx | 0 .../web/components}/auth/turnstile-widget.tsx | 0 .../components}/charts/BatteryLifeChart.tsx | 0 .../web/components}/charts/DeviceDonut.tsx | 0 .../web/components}/charts/EChartWrapper.tsx | 0 .../web/components}/charts/FpsBoxplot.tsx | 0 .../web/components}/charts/FpsRangeChart.tsx | 0 .../charts/HistoricalAreaChart.tsx | 0 .../charts/PerformanceTierChart.tsx | 0 .../charts/StabilityScatterChart.tsx | 0 .../components}/charts/UpscalerBarChart.tsx | 0 .../web/components}/comments/comment-item.tsx | 0 .../components}/comments/comment-section.tsx | 0 .../components}/community-suggestion-form.tsx | 0 .../compare/fps-comparison-chart.tsx | 0 .../web/components}/compare/game-selector.tsx | 0 .../components}/compare/stability-radar.tsx | 0 .../components}/compare/stats-comparison.tsx | 0 .../web/components}/filter-drawer.tsx | 0 .../components}/manage/analytics-client.tsx | 0 .../components}/manage/dashboard-charts.tsx | 0 .../components}/manage/dashboard-overview.tsx | 0 .../web/components}/manage/manage-sidebar.tsx | 0 .../web/components}/navbar.tsx | 0 .../web/components}/playability-badge.tsx | 0 .../components}/profile/contribution-list.tsx | 0 .../components}/profile/profile-header.tsx | 0 .../profile/profile-photo-upload.tsx | 0 .../profile/settings-accounts-tab.tsx | 0 .../profile/settings-container.tsx | 0 .../profile/settings-profile-tab.tsx | 0 .../profile/settings-security-tab.tsx | 0 .../web/components}/profile/stats-row.tsx | 0 .../web/components}/saved-filters.tsx | 0 .../saved-games/bookmark-button.tsx | 0 .../saved-games/saved-games-grid.tsx | 0 .../web/components}/steam-reviews.tsx | 0 .../web/components}/tiptap-editor.tsx | 0 .../web/components}/tiptap-renderer.tsx | 0 .../components}/ui/modal/confirm-dialog.tsx | 0 .../web/components}/ui/modal/index.ts | 0 .../web/components}/ui/modal/modal.tsx | 0 .../components}/ui/screenshot-lightbox.tsx | 0 .../web/components}/updates/chapter-nav.tsx | 0 .../updates/reading-progress-bar.tsx | 0 .../web/components}/updates/update-card.tsx | 0 .../web/components}/updates/update-viewer.tsx | 0 .../components}/wizard/game-entry-wizard.tsx | 0 .../wizard/non-steam-edit-form.tsx | 0 .../components}/wizard/non-steam-wizard.tsx | 0 .../components}/wizard/settings-editor.tsx | 0 .../web/components}/wizard/step-indicator.tsx | 0 .../wizard/steps/anti-cheat-step.tsx | 0 .../wizard/steps/environment-step.tsx | 0 .../wizard/steps/hardware-step.tsx | 0 .../web/components}/wizard/steps/index.ts | 0 .../steps/non-steam-basic-info-step.tsx | 0 .../wizard/steps/non-steam-image-step.tsx | 0 .../wizard/steps/non-steam-platform-step.tsx | 0 .../wizard/steps/non-steam-review-step.tsx | 0 .../wizard/steps/performance-step.tsx | 0 .../components}/wizard/steps/review-step.tsx | 0 .../wizard/steps/settings-step.tsx | 0 .../components}/wizard/steps/setup-step.tsx | 0 .../content}/updates/2026-04-30-v2026.0.95.md | 0 .../content}/updates/2026-05-01-v2026.0.96.md | 0 .../content}/updates/2026-05-06-v2026.0.97.md | 0 .../updates/2026-05-09-v2026.0.97.1.md | 0 .../content}/updates/2026-05-09-v2026.0.98.md | 0 .../content}/updates/2026-05-09-v2026.0.99.md | 0 .../updates/2026-05-10-v2026.0.100.md | 0 .../updates/2026-05-14-v2026.0.101.md | 0 .../updates/2026-05-16-v2026.0.102.md | 0 .../content}/updates/2026-05-20-v2026.2.0.md | 0 .../content}/updates/2026-05-25-v2026.2.1.md | 0 .../content}/updates/2026-05-27-v2026.2.2.md | 0 .../web/drizzle.config.ts | 0 .../web/drizzle}/0000_crazy_nick_fury.sql | 0 .../web/drizzle}/0001_big_smiling_tiger.sql | 0 .../web/drizzle}/0002_square_slayback.sql | 0 .../web/drizzle}/0004_aspiring_meggan.sql | 0 .../0005_add_preset_performance_link.sql | 0 .../drizzle}/0005_brief_marvel_zombies.sql | 0 .../0006_upscaler_battery_custom_system.sql | 0 .../web/drizzle}/0007_mighty_electro.sql | 0 .../web/drizzle}/0008_slim_jubilee.sql | 0 .../web/drizzle}/0009_flawless_meltdown.sql | 0 .../web/drizzle}/0010_soft_wilson_fisk.sql | 0 ...1_rename-handled-to-handheld-add-image.sql | 0 .../web/drizzle}/0011_talented_bloodstorm.sql | 0 .../drizzle}/0012_clever_thunderbolt_ross.sql | 0 .../web/drizzle}/0013_supreme_purple_man.sql | 0 .../web/drizzle}/0014_purple_firebird.sql | 0 .../web/drizzle}/0015_nappy_the_phantom.sql | 0 .../web/drizzle}/0016_zippy_eternals.sql | 0 .../web/drizzle}/0017_polite_speed_demon.sql | 0 .../web/drizzle}/0018_slimy_proemial_gods.sql | 0 .../web/drizzle}/0019_tiresome_genesis.sql | 0 .../web/drizzle}/0020_flaky_lilith.sql | 0 .../drizzle}/0021_demonic_texas_twister.sql | 0 .../web/drizzle}/0022_married_zeigeist.sql | 0 .../web/drizzle}/0023_married_avengers.sql | 0 .../web/drizzle}/0024_violet_psynapse.sql | 0 .../web/drizzle}/0025_flowery_molten_man.sql | 0 .../web/drizzle}/0026_polite_drax.sql | 0 .../web/drizzle}/meta/0000_snapshot.json | 0 .../web/drizzle}/meta/0001_snapshot.json | 0 .../web/drizzle}/meta/0002_snapshot.json | 0 .../web/drizzle}/meta/0005_snapshot.json | 0 .../web/drizzle}/meta/0007_snapshot.json | 0 .../web/drizzle}/meta/0008_snapshot.json | 0 .../web/drizzle}/meta/0009_snapshot.json | 0 .../web/drizzle}/meta/0010_snapshot.json | 0 .../web/drizzle}/meta/0011_snapshot.json | 0 .../web/drizzle}/meta/0012_snapshot.json | 0 .../web/drizzle}/meta/0013_snapshot.json | 0 .../web/drizzle}/meta/0014_snapshot.json | 0 .../web/drizzle}/meta/0015_snapshot.json | 0 .../web/drizzle}/meta/0016_snapshot.json | 0 .../web/drizzle}/meta/0017_snapshot.json | 0 .../web/drizzle}/meta/0018_snapshot.json | 0 .../web/drizzle}/meta/0019_snapshot.json | 0 .../web/drizzle}/meta/0020_snapshot.json | 0 .../web/drizzle}/meta/0021_snapshot.json | 0 .../web/drizzle}/meta/0022_snapshot.json | 0 .../web/drizzle}/meta/0023_snapshot.json | 0 .../web/drizzle}/meta/0024_snapshot.json | 0 .../web/drizzle}/meta/0025_snapshot.json | 0 .../web/drizzle}/meta/0026_snapshot.json | 0 .../web/drizzle}/meta/_journal.json | 0 .../web/eslint.config.mjs | 0 .../web/lib}/__tests__/sitemap-utils.test.ts | 0 .../web/lib}/__tests__/version-bump.test.ts | 0 .../web/lib}/api/__tests__/auto-pin.test.ts | 0 .../lib}/api/__tests__/check-email.test.ts | 0 .../api/__tests__/domain-block-api.test.ts | 0 .../lib}/api/__tests__/openapi-fixes.test.ts | 0 .../api/__tests__/rate-limit-removal.test.ts | 0 {lib => apps/web/lib}/api/admin-analytics.ts | 0 {lib => apps/web/lib}/api/admin-comments.ts | 0 .../web/lib}/api/admin-performance.ts | 0 {lib => apps/web/lib}/api/admin-reports.ts | 0 {lib => apps/web/lib}/api/admin-storage.ts | 0 {lib => apps/web/lib}/api/app.ts | 0 {lib => apps/web/lib}/api/auto-pin.ts | 0 {lib => apps/web/lib}/api/comments.ts | 0 .../web/lib}/api/community-suggestions.ts | 0 {lib => apps/web/lib}/api/compare.ts | 0 {lib => apps/web/lib}/api/contact.ts | 0 {lib => apps/web/lib}/api/cron.ts | 0 {lib => apps/web/lib}/api/crud-builder.ts | 0 {lib => apps/web/lib}/api/dashboard-public.ts | 0 {lib => apps/web/lib}/api/dashboard.ts | 0 {lib => apps/web/lib}/api/game-stats.ts | 0 {lib => apps/web/lib}/api/game-stub.ts | 0 {lib => apps/web/lib}/api/games-listing.ts | 0 {lib => apps/web/lib}/api/games-manual.ts | 0 .../web/lib}/api/games-performance.ts | 0 {lib => apps/web/lib}/api/games.ts | 0 {lib => apps/web/lib}/api/hardware-stats.ts | 0 {lib => apps/web/lib}/api/hardware.ts | 0 {lib => apps/web/lib}/api/health.ts | 0 {lib => apps/web/lib}/api/index.ts | 0 {lib => apps/web/lib}/api/mobile.ts | 0 .../web/lib}/api/performance-submit.ts | 0 {lib => apps/web/lib}/api/performance.ts | 0 {lib => apps/web/lib}/api/playability.ts | 0 {lib => apps/web/lib}/api/profile-photo.ts | 0 {lib => apps/web/lib}/api/reports.ts | 0 {lib => apps/web/lib}/api/saved-filters.ts | 0 {lib => apps/web/lib}/api/saved-games.ts | 0 {lib => apps/web/lib}/api/screenshots.ts | 0 {lib => apps/web/lib}/api/search-unified.ts | 0 {lib => apps/web/lib}/api/steam-reviews.ts | 0 {lib => apps/web/lib}/api/steam-search.ts | 0 {lib => apps/web/lib}/api/steamdb-version.ts | 0 {lib => apps/web/lib}/api/steamgrid-proxy.ts | 0 {lib => apps/web/lib}/api/user.ts | 0 {lib => apps/web/lib}/api/version-test.ts | 0 {lib => apps/web/lib}/auth-client.ts | 0 {lib => apps/web/lib}/auth.ts | 0 .../web/lib}/auth/__tests__/auth.test.ts | 0 .../lib}/auth/__tests__/domain-block.test.ts | 0 .../web/lib}/auth/__tests__/guard.test.ts | 0 .../auth/__tests__/password-strength.test.ts | 0 .../lib}/auth/__tests__/validation.test.ts | 0 {lib => apps/web/lib}/auth/domain-block.ts | 0 {lib => apps/web/lib}/auth/email.ts | 0 {lib => apps/web/lib}/auth/guard.ts | 0 .../web/lib}/auth/password-strength.ts | 0 {lib => apps/web/lib}/auth/permissions.ts | 0 {lib => apps/web/lib}/auth/test.ts | 0 {lib => apps/web/lib}/auth/validation.ts | 0 {lib => apps/web/lib}/db/index.ts | 0 .../web/lib}/db/migrations/backfill-slugs.ts | 0 {lib => apps/web/lib}/db/schema/auth.ts | 0 .../lib}/db/schema/community-suggestions.ts | 0 .../web/lib}/db/schema/entryScreenshots.ts | 0 .../web/lib}/db/schema/gameComments.ts | 0 .../web/lib}/db/schema/gamePlatformSupport.ts | 0 .../web/lib}/db/schema/gameVersions.ts | 0 {lib => apps/web/lib}/db/schema/games.ts | 0 {lib => apps/web/lib}/db/schema/hardware.ts | 0 {lib => apps/web/lib}/db/schema/index.ts | 0 .../web/lib}/db/schema/performanceEntries.ts | 0 {lib => apps/web/lib}/db/schema/reports.ts | 0 .../web/lib}/db/schema/saved-filters.ts | 0 {lib => apps/web/lib}/db/schema/savedGames.ts | 0 {lib => apps/web/lib}/db/schema/storage.ts | 0 {lib => apps/web/lib}/db/search.ts | 0 {lib => apps/web/lib}/db/seed.ts | 0 {lib => apps/web/lib}/eden.ts | 0 .../web/lib}/hooks/use-gamepad-navigation.ts | 0 {lib => apps/web/lib}/hooks/useDebounce.ts | 0 {lib => apps/web/lib}/image-processing.ts | 0 {lib => apps/web/lib}/routes.ts | 0 {lib => apps/web/lib}/sitemap-utils.ts | 0 .../steam/__tests__/ensure-steam-game.test.ts | 0 {lib => apps/web/lib}/steam/sync.ts | 0 .../web/lib}/steamdb/__tests__/scrape.test.ts | 0 {lib => apps/web/lib}/steamdb/cache.ts | 0 {lib => apps/web/lib}/steamdb/scrape.ts | 0 {lib => apps/web/lib}/storage/index.ts | 0 {lib => apps/web/lib}/storage/r2-client.ts | 0 {lib => apps/web/lib}/updates.ts | 0 {lib => apps/web/lib}/utils.ts | 0 .../web/lib}/utils/__tests__/seo.test.ts | 0 .../web/lib}/utils/__tests__/slug.test.ts | 0 {lib => apps/web/lib}/utils/seo.ts | 0 {lib => apps/web/lib}/utils/slug.ts | 0 .../lib}/version-fetchers/community-hub.ts | 0 .../web/lib}/version-fetchers/index.ts | 0 .../web/lib}/version-fetchers/store-api.ts | 0 .../web/lib}/version-fetchers/store-page.ts | 0 .../web/lib}/version-fetchers/types.ts | 0 .../web/lib}/version-fetchers/uptodate-api.ts | 0 next.config.ts => apps/web/next.config.ts | 0 apps/web/package.json | 88 ++++++++++++++ .../web/postcss.config.mjs | 0 proxy.ts => apps/web/proxy.ts | 0 .../.well-known/apple-app-site-association | 0 .../web/public}/.well-known/assetlinks.json | 0 {public => apps/web/public}/icon-192.png | Bin {public => apps/web/public}/icon-512.png | Bin {public => apps/web/public}/icon.png | Bin {public => apps/web/public}/offline.html | 0 tsconfig.json => apps/web/tsconfig.json | 0 {types => apps/web/types}/api.ts | 0 vitest.config.ts => apps/web/vitest.config.ts | 0 bun.lock | 111 +++++++++++++++++- package.json | 97 +++------------ packages/shared/package.json | 11 ++ packages/shared/src/index.ts | 90 ++++++++++++++ packages/shared/tsconfig.json | 15 +++ plugins/decky-vault/package.json | 19 +++ plugins/decky-vault/plugin.json | 9 ++ plugins/decky-vault/rollup.config.js | 20 ++++ plugins/decky-vault/src/index.tsx | 32 +++++ plugins/decky-vault/src/types.d.ts | 32 +++++ plugins/decky-vault/tsconfig.json | 19 +++ 345 files changed, 488 insertions(+), 126 deletions(-) rename .env.example => apps/web/.env.example (100%) rename CHANGELOG.md => apps/web/CHANGELOG.md (100%) rename {__tests__ => apps/web/__tests__}/proxy.test.ts (100%) rename {app => apps/web/app}/(auth)/forgot-password/page.tsx (100%) rename {app => apps/web/app}/(auth)/layout.tsx (100%) rename {app => apps/web/app}/(auth)/login/page.tsx (100%) rename {app => apps/web/app}/(auth)/reset-password/page.tsx (100%) rename {app => apps/web/app}/(auth)/signup/page.tsx (100%) rename {app => apps/web/app}/(manage)/layout.tsx (100%) rename {app => apps/web/app}/(manage)/manage/analytics/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/benchmarks/benchmarks-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/benchmarks/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/comments/comments-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/comments/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/games/games-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/games/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/hardware/hardware-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/hardware/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/reports/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/reports/reports-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/storage/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/storage/storage-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/suggestions/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/suggestions/suggestions-client.tsx (100%) rename {app => apps/web/app}/(manage)/manage/users/page.tsx (100%) rename {app => apps/web/app}/(manage)/manage/users/users-client.tsx (100%) rename {app => apps/web/app}/__tests__/metadata.test.ts (100%) rename {app => apps/web/app}/__tests__/sitemap.integration.test.ts (100%) rename {app => apps/web/app}/__tests__/sitemap.test.ts (100%) rename {app => apps/web/app}/api/[[...slugs]]/route.ts (100%) rename {app => apps/web/app}/app/[steamid]/page.tsx (100%) rename {app => apps/web/app}/app/__tests__/redirect.test.ts (100%) rename {app => apps/web/app}/compare/layout.tsx (100%) rename {app => apps/web/app}/compare/page.tsx (100%) rename {app => apps/web/app}/components/PlatformIcons.tsx (100%) rename {app => apps/web/app}/contact/layout.tsx (100%) rename {app => apps/web/app}/contact/page.tsx (100%) rename {app => apps/web/app}/devices/[slug]/device-detail-client.tsx (100%) rename {app => apps/web/app}/devices/[slug]/opengraph-image.tsx (100%) rename {app => apps/web/app}/devices/[slug]/page.tsx (100%) rename {app => apps/web/app}/devices/page-client.tsx (100%) rename {app => apps/web/app}/devices/page.tsx (100%) rename {app => apps/web/app}/game/[id]/edit/page.tsx (100%) rename {app => apps/web/app}/game/[id]/game-page-client.tsx (100%) rename {app => apps/web/app}/game/[id]/opengraph-image.tsx (100%) rename {app => apps/web/app}/game/[id]/page.tsx (100%) rename {app => apps/web/app}/game/[id]/preset-detail-modal.tsx (100%) rename {app => apps/web/app}/game/[id]/submit/page.tsx (100%) rename {app => apps/web/app}/game/add/page.tsx (100%) rename {app => apps/web/app}/games/games-page-client.tsx (100%) rename {app => apps/web/app}/games/page.tsx (100%) rename {app => apps/web/app}/globals.css (100%) rename {app => apps/web/app}/icon.png (100%) rename {app => apps/web/app}/layout.tsx (100%) rename {app => apps/web/app}/manifest.ts (100%) rename {app => apps/web/app}/not-found.tsx (100%) rename {app => apps/web/app}/opengraph-image.tsx (100%) rename {app => apps/web/app}/page.tsx (100%) rename {app => apps/web/app}/profile/[id]/page.tsx (100%) rename {app => apps/web/app}/profile/[id]/profile-page-client.tsx (100%) rename {app => apps/web/app}/profile/layout.tsx (100%) rename {app => apps/web/app}/profile/page.tsx (100%) rename {app => apps/web/app}/robots.ts (100%) rename {app => apps/web/app}/search/layout.tsx (100%) rename {app => apps/web/app}/search/page.tsx (100%) rename {app => apps/web/app}/sitemap.ts (100%) rename {app => apps/web/app}/sw.ts (100%) rename {app => apps/web/app}/test-version-fetchers/page.tsx (100%) rename {app => apps/web/app}/twitter-image.tsx (100%) rename {app => apps/web/app}/updates/[slug]/page.tsx (100%) rename {app => apps/web/app}/updates/page.tsx (100%) rename {components => apps/web/components}/anti-cheat-badge.tsx (100%) rename {components => apps/web/components}/auth/__tests__/placeholder.test.tsx (100%) rename {components => apps/web/components}/auth/forgot-password-form.tsx (100%) rename {components => apps/web/components}/auth/login-form.tsx (100%) rename {components => apps/web/components}/auth/otp-input.tsx (100%) rename {components => apps/web/components}/auth/otp-verification-step.tsx (100%) rename {components => apps/web/components}/auth/passkey-setup-step.tsx (100%) rename {components => apps/web/components}/auth/password-strength.tsx (100%) rename {components => apps/web/components}/auth/reset-password-form.tsx (100%) rename {components => apps/web/components}/auth/signup-form-step.tsx (100%) rename {components => apps/web/components}/auth/signup-wizard.tsx (100%) rename {components => apps/web/components}/auth/social-buttons.tsx (100%) rename {components => apps/web/components}/auth/step-indicator.tsx (100%) rename {components => apps/web/components}/auth/turnstile-widget.tsx (100%) rename {components => apps/web/components}/charts/BatteryLifeChart.tsx (100%) rename {components => apps/web/components}/charts/DeviceDonut.tsx (100%) rename {components => apps/web/components}/charts/EChartWrapper.tsx (100%) rename {components => apps/web/components}/charts/FpsBoxplot.tsx (100%) rename {components => apps/web/components}/charts/FpsRangeChart.tsx (100%) rename {components => apps/web/components}/charts/HistoricalAreaChart.tsx (100%) rename {components => apps/web/components}/charts/PerformanceTierChart.tsx (100%) rename {components => apps/web/components}/charts/StabilityScatterChart.tsx (100%) rename {components => apps/web/components}/charts/UpscalerBarChart.tsx (100%) rename {components => apps/web/components}/comments/comment-item.tsx (100%) rename {components => apps/web/components}/comments/comment-section.tsx (100%) rename {components => apps/web/components}/community-suggestion-form.tsx (100%) rename {components => apps/web/components}/compare/fps-comparison-chart.tsx (100%) rename {components => apps/web/components}/compare/game-selector.tsx (100%) rename {components => apps/web/components}/compare/stability-radar.tsx (100%) rename {components => apps/web/components}/compare/stats-comparison.tsx (100%) rename {components => apps/web/components}/filter-drawer.tsx (100%) rename {components => apps/web/components}/manage/analytics-client.tsx (100%) rename {components => apps/web/components}/manage/dashboard-charts.tsx (100%) rename {components => apps/web/components}/manage/dashboard-overview.tsx (100%) rename {components => apps/web/components}/manage/manage-sidebar.tsx (100%) rename {components => apps/web/components}/navbar.tsx (100%) rename {components => apps/web/components}/playability-badge.tsx (100%) rename {components => apps/web/components}/profile/contribution-list.tsx (100%) rename {components => apps/web/components}/profile/profile-header.tsx (100%) rename {components => apps/web/components}/profile/profile-photo-upload.tsx (100%) rename {components => apps/web/components}/profile/settings-accounts-tab.tsx (100%) rename {components => apps/web/components}/profile/settings-container.tsx (100%) rename {components => apps/web/components}/profile/settings-profile-tab.tsx (100%) rename {components => apps/web/components}/profile/settings-security-tab.tsx (100%) rename {components => apps/web/components}/profile/stats-row.tsx (100%) rename {components => apps/web/components}/saved-filters.tsx (100%) rename {components => apps/web/components}/saved-games/bookmark-button.tsx (100%) rename {components => apps/web/components}/saved-games/saved-games-grid.tsx (100%) rename {components => apps/web/components}/steam-reviews.tsx (100%) rename {components => apps/web/components}/tiptap-editor.tsx (100%) rename {components => apps/web/components}/tiptap-renderer.tsx (100%) rename {components => apps/web/components}/ui/modal/confirm-dialog.tsx (100%) rename {components => apps/web/components}/ui/modal/index.ts (100%) rename {components => apps/web/components}/ui/modal/modal.tsx (100%) rename {components => apps/web/components}/ui/screenshot-lightbox.tsx (100%) rename {components => apps/web/components}/updates/chapter-nav.tsx (100%) rename {components => apps/web/components}/updates/reading-progress-bar.tsx (100%) rename {components => apps/web/components}/updates/update-card.tsx (100%) rename {components => apps/web/components}/updates/update-viewer.tsx (100%) rename {components => apps/web/components}/wizard/game-entry-wizard.tsx (100%) rename {components => apps/web/components}/wizard/non-steam-edit-form.tsx (100%) rename {components => apps/web/components}/wizard/non-steam-wizard.tsx (100%) rename {components => apps/web/components}/wizard/settings-editor.tsx (100%) rename {components => apps/web/components}/wizard/step-indicator.tsx (100%) rename {components => apps/web/components}/wizard/steps/anti-cheat-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/environment-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/hardware-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/index.ts (100%) rename {components => apps/web/components}/wizard/steps/non-steam-basic-info-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/non-steam-image-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/non-steam-platform-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/non-steam-review-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/performance-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/review-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/settings-step.tsx (100%) rename {components => apps/web/components}/wizard/steps/setup-step.tsx (100%) rename {content => apps/web/content}/updates/2026-04-30-v2026.0.95.md (100%) rename {content => apps/web/content}/updates/2026-05-01-v2026.0.96.md (100%) rename {content => apps/web/content}/updates/2026-05-06-v2026.0.97.md (100%) rename {content => apps/web/content}/updates/2026-05-09-v2026.0.97.1.md (100%) rename {content => apps/web/content}/updates/2026-05-09-v2026.0.98.md (100%) rename {content => apps/web/content}/updates/2026-05-09-v2026.0.99.md (100%) rename {content => apps/web/content}/updates/2026-05-10-v2026.0.100.md (100%) rename {content => apps/web/content}/updates/2026-05-14-v2026.0.101.md (100%) rename {content => apps/web/content}/updates/2026-05-16-v2026.0.102.md (100%) rename {content => apps/web/content}/updates/2026-05-20-v2026.2.0.md (100%) rename {content => apps/web/content}/updates/2026-05-25-v2026.2.1.md (100%) rename {content => apps/web/content}/updates/2026-05-27-v2026.2.2.md (100%) rename drizzle.config.ts => apps/web/drizzle.config.ts (100%) rename {drizzle => apps/web/drizzle}/0000_crazy_nick_fury.sql (100%) rename {drizzle => apps/web/drizzle}/0001_big_smiling_tiger.sql (100%) rename {drizzle => apps/web/drizzle}/0002_square_slayback.sql (100%) rename {drizzle => apps/web/drizzle}/0004_aspiring_meggan.sql (100%) rename {drizzle => apps/web/drizzle}/0005_add_preset_performance_link.sql (100%) rename {drizzle => apps/web/drizzle}/0005_brief_marvel_zombies.sql (100%) rename {drizzle => apps/web/drizzle}/0006_upscaler_battery_custom_system.sql (100%) rename {drizzle => apps/web/drizzle}/0007_mighty_electro.sql (100%) rename {drizzle => apps/web/drizzle}/0008_slim_jubilee.sql (100%) rename {drizzle => apps/web/drizzle}/0009_flawless_meltdown.sql (100%) rename {drizzle => apps/web/drizzle}/0010_soft_wilson_fisk.sql (100%) rename {drizzle => apps/web/drizzle}/0011_rename-handled-to-handheld-add-image.sql (100%) rename {drizzle => apps/web/drizzle}/0011_talented_bloodstorm.sql (100%) rename {drizzle => apps/web/drizzle}/0012_clever_thunderbolt_ross.sql (100%) rename {drizzle => apps/web/drizzle}/0013_supreme_purple_man.sql (100%) rename {drizzle => apps/web/drizzle}/0014_purple_firebird.sql (100%) rename {drizzle => apps/web/drizzle}/0015_nappy_the_phantom.sql (100%) rename {drizzle => apps/web/drizzle}/0016_zippy_eternals.sql (100%) rename {drizzle => apps/web/drizzle}/0017_polite_speed_demon.sql (100%) rename {drizzle => apps/web/drizzle}/0018_slimy_proemial_gods.sql (100%) rename {drizzle => apps/web/drizzle}/0019_tiresome_genesis.sql (100%) rename {drizzle => apps/web/drizzle}/0020_flaky_lilith.sql (100%) rename {drizzle => apps/web/drizzle}/0021_demonic_texas_twister.sql (100%) rename {drizzle => apps/web/drizzle}/0022_married_zeigeist.sql (100%) rename {drizzle => apps/web/drizzle}/0023_married_avengers.sql (100%) rename {drizzle => apps/web/drizzle}/0024_violet_psynapse.sql (100%) rename {drizzle => apps/web/drizzle}/0025_flowery_molten_man.sql (100%) rename {drizzle => apps/web/drizzle}/0026_polite_drax.sql (100%) rename {drizzle => apps/web/drizzle}/meta/0000_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0001_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0002_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0005_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0007_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0008_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0009_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0010_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0011_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0012_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0013_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0014_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0015_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0016_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0017_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0018_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0019_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0020_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0021_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0022_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0023_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0024_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0025_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/0026_snapshot.json (100%) rename {drizzle => apps/web/drizzle}/meta/_journal.json (100%) rename eslint.config.mjs => apps/web/eslint.config.mjs (100%) rename {lib => apps/web/lib}/__tests__/sitemap-utils.test.ts (100%) rename {lib => apps/web/lib}/__tests__/version-bump.test.ts (100%) rename {lib => apps/web/lib}/api/__tests__/auto-pin.test.ts (100%) rename {lib => apps/web/lib}/api/__tests__/check-email.test.ts (100%) rename {lib => apps/web/lib}/api/__tests__/domain-block-api.test.ts (100%) rename {lib => apps/web/lib}/api/__tests__/openapi-fixes.test.ts (100%) rename {lib => apps/web/lib}/api/__tests__/rate-limit-removal.test.ts (100%) rename {lib => apps/web/lib}/api/admin-analytics.ts (100%) rename {lib => apps/web/lib}/api/admin-comments.ts (100%) rename {lib => apps/web/lib}/api/admin-performance.ts (100%) rename {lib => apps/web/lib}/api/admin-reports.ts (100%) rename {lib => apps/web/lib}/api/admin-storage.ts (100%) rename {lib => apps/web/lib}/api/app.ts (100%) rename {lib => apps/web/lib}/api/auto-pin.ts (100%) rename {lib => apps/web/lib}/api/comments.ts (100%) rename {lib => apps/web/lib}/api/community-suggestions.ts (100%) rename {lib => apps/web/lib}/api/compare.ts (100%) rename {lib => apps/web/lib}/api/contact.ts (100%) rename {lib => apps/web/lib}/api/cron.ts (100%) rename {lib => apps/web/lib}/api/crud-builder.ts (100%) rename {lib => apps/web/lib}/api/dashboard-public.ts (100%) rename {lib => apps/web/lib}/api/dashboard.ts (100%) rename {lib => apps/web/lib}/api/game-stats.ts (100%) rename {lib => apps/web/lib}/api/game-stub.ts (100%) rename {lib => apps/web/lib}/api/games-listing.ts (100%) rename {lib => apps/web/lib}/api/games-manual.ts (100%) rename {lib => apps/web/lib}/api/games-performance.ts (100%) rename {lib => apps/web/lib}/api/games.ts (100%) rename {lib => apps/web/lib}/api/hardware-stats.ts (100%) rename {lib => apps/web/lib}/api/hardware.ts (100%) rename {lib => apps/web/lib}/api/health.ts (100%) rename {lib => apps/web/lib}/api/index.ts (100%) rename {lib => apps/web/lib}/api/mobile.ts (100%) rename {lib => apps/web/lib}/api/performance-submit.ts (100%) rename {lib => apps/web/lib}/api/performance.ts (100%) rename {lib => apps/web/lib}/api/playability.ts (100%) rename {lib => apps/web/lib}/api/profile-photo.ts (100%) rename {lib => apps/web/lib}/api/reports.ts (100%) rename {lib => apps/web/lib}/api/saved-filters.ts (100%) rename {lib => apps/web/lib}/api/saved-games.ts (100%) rename {lib => apps/web/lib}/api/screenshots.ts (100%) rename {lib => apps/web/lib}/api/search-unified.ts (100%) rename {lib => apps/web/lib}/api/steam-reviews.ts (100%) rename {lib => apps/web/lib}/api/steam-search.ts (100%) rename {lib => apps/web/lib}/api/steamdb-version.ts (100%) rename {lib => apps/web/lib}/api/steamgrid-proxy.ts (100%) rename {lib => apps/web/lib}/api/user.ts (100%) rename {lib => apps/web/lib}/api/version-test.ts (100%) rename {lib => apps/web/lib}/auth-client.ts (100%) rename {lib => apps/web/lib}/auth.ts (100%) rename {lib => apps/web/lib}/auth/__tests__/auth.test.ts (100%) rename {lib => apps/web/lib}/auth/__tests__/domain-block.test.ts (100%) rename {lib => apps/web/lib}/auth/__tests__/guard.test.ts (100%) rename {lib => apps/web/lib}/auth/__tests__/password-strength.test.ts (100%) rename {lib => apps/web/lib}/auth/__tests__/validation.test.ts (100%) rename {lib => apps/web/lib}/auth/domain-block.ts (100%) rename {lib => apps/web/lib}/auth/email.ts (100%) rename {lib => apps/web/lib}/auth/guard.ts (100%) rename {lib => apps/web/lib}/auth/password-strength.ts (100%) rename {lib => apps/web/lib}/auth/permissions.ts (100%) rename {lib => apps/web/lib}/auth/test.ts (100%) rename {lib => apps/web/lib}/auth/validation.ts (100%) rename {lib => apps/web/lib}/db/index.ts (100%) rename {lib => apps/web/lib}/db/migrations/backfill-slugs.ts (100%) rename {lib => apps/web/lib}/db/schema/auth.ts (100%) rename {lib => apps/web/lib}/db/schema/community-suggestions.ts (100%) rename {lib => apps/web/lib}/db/schema/entryScreenshots.ts (100%) rename {lib => apps/web/lib}/db/schema/gameComments.ts (100%) rename {lib => apps/web/lib}/db/schema/gamePlatformSupport.ts (100%) rename {lib => apps/web/lib}/db/schema/gameVersions.ts (100%) rename {lib => apps/web/lib}/db/schema/games.ts (100%) rename {lib => apps/web/lib}/db/schema/hardware.ts (100%) rename {lib => apps/web/lib}/db/schema/index.ts (100%) rename {lib => apps/web/lib}/db/schema/performanceEntries.ts (100%) rename {lib => apps/web/lib}/db/schema/reports.ts (100%) rename {lib => apps/web/lib}/db/schema/saved-filters.ts (100%) rename {lib => apps/web/lib}/db/schema/savedGames.ts (100%) rename {lib => apps/web/lib}/db/schema/storage.ts (100%) rename {lib => apps/web/lib}/db/search.ts (100%) rename {lib => apps/web/lib}/db/seed.ts (100%) rename {lib => apps/web/lib}/eden.ts (100%) rename {lib => apps/web/lib}/hooks/use-gamepad-navigation.ts (100%) rename {lib => apps/web/lib}/hooks/useDebounce.ts (100%) rename {lib => apps/web/lib}/image-processing.ts (100%) rename {lib => apps/web/lib}/routes.ts (100%) rename {lib => apps/web/lib}/sitemap-utils.ts (100%) rename {lib => apps/web/lib}/steam/__tests__/ensure-steam-game.test.ts (100%) rename {lib => apps/web/lib}/steam/sync.ts (100%) rename {lib => apps/web/lib}/steamdb/__tests__/scrape.test.ts (100%) rename {lib => apps/web/lib}/steamdb/cache.ts (100%) rename {lib => apps/web/lib}/steamdb/scrape.ts (100%) rename {lib => apps/web/lib}/storage/index.ts (100%) rename {lib => apps/web/lib}/storage/r2-client.ts (100%) rename {lib => apps/web/lib}/updates.ts (100%) rename {lib => apps/web/lib}/utils.ts (100%) rename {lib => apps/web/lib}/utils/__tests__/seo.test.ts (100%) rename {lib => apps/web/lib}/utils/__tests__/slug.test.ts (100%) rename {lib => apps/web/lib}/utils/seo.ts (100%) rename {lib => apps/web/lib}/utils/slug.ts (100%) rename {lib => apps/web/lib}/version-fetchers/community-hub.ts (100%) rename {lib => apps/web/lib}/version-fetchers/index.ts (100%) rename {lib => apps/web/lib}/version-fetchers/store-api.ts (100%) rename {lib => apps/web/lib}/version-fetchers/store-page.ts (100%) rename {lib => apps/web/lib}/version-fetchers/types.ts (100%) rename {lib => apps/web/lib}/version-fetchers/uptodate-api.ts (100%) rename next.config.ts => apps/web/next.config.ts (100%) create mode 100644 apps/web/package.json rename postcss.config.mjs => apps/web/postcss.config.mjs (100%) rename proxy.ts => apps/web/proxy.ts (100%) rename {public => apps/web/public}/.well-known/apple-app-site-association (100%) rename {public => apps/web/public}/.well-known/assetlinks.json (100%) rename {public => apps/web/public}/icon-192.png (100%) rename {public => apps/web/public}/icon-512.png (100%) rename {public => apps/web/public}/icon.png (100%) rename {public => apps/web/public}/offline.html (100%) rename tsconfig.json => apps/web/tsconfig.json (100%) rename {types => apps/web/types}/api.ts (100%) rename vitest.config.ts => apps/web/vitest.config.ts (100%) create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 plugins/decky-vault/package.json create mode 100644 plugins/decky-vault/plugin.json create mode 100644 plugins/decky-vault/rollup.config.js create mode 100644 plugins/decky-vault/src/index.tsx create mode 100644 plugins/decky-vault/src/types.d.ts create mode 100644 plugins/decky-vault/tsconfig.json diff --git a/.gitignore b/.gitignore index 9aa9f27..ef07c1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,56 +1,45 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +node_modules/ +.next/ +*.tsbuildinfo +dist/ -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions +# Environment +.env +.env.local +.env.*.local +.env.production -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc +# OS .DS_Store -*.pem +*.swp +*.swo -# debug +# IDE +.idea/ +.vscode/ +*.sublime-* + +# Certificates (generated locally) +certificates/ + +# next.js auto-generated +next-env.d.ts + +# serwist build artifact +**/public/sw.js + +# debug logs npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* -!.env.example - # vercel .vercel -# typescript -*.tsbuildinfo -next-env.d.ts - -# serwist build artifact -public/sw.js - -/docs/superpowers - -# git worktrees -.worktrees/ +# pi agent .pi/ - .superpowers/ -# temporary files -post.md \ No newline at end of file +# docs/superpowers +/docs/superpowers \ No newline at end of file diff --git a/.env.example b/apps/web/.env.example similarity index 100% rename from .env.example rename to apps/web/.env.example diff --git a/CHANGELOG.md b/apps/web/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to apps/web/CHANGELOG.md diff --git a/__tests__/proxy.test.ts b/apps/web/__tests__/proxy.test.ts similarity index 100% rename from __tests__/proxy.test.ts rename to apps/web/__tests__/proxy.test.ts diff --git a/app/(auth)/forgot-password/page.tsx b/apps/web/app/(auth)/forgot-password/page.tsx similarity index 100% rename from app/(auth)/forgot-password/page.tsx rename to apps/web/app/(auth)/forgot-password/page.tsx diff --git a/app/(auth)/layout.tsx b/apps/web/app/(auth)/layout.tsx similarity index 100% rename from app/(auth)/layout.tsx rename to apps/web/app/(auth)/layout.tsx diff --git a/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx similarity index 100% rename from app/(auth)/login/page.tsx rename to apps/web/app/(auth)/login/page.tsx diff --git a/app/(auth)/reset-password/page.tsx b/apps/web/app/(auth)/reset-password/page.tsx similarity index 100% rename from app/(auth)/reset-password/page.tsx rename to apps/web/app/(auth)/reset-password/page.tsx diff --git a/app/(auth)/signup/page.tsx b/apps/web/app/(auth)/signup/page.tsx similarity index 100% rename from app/(auth)/signup/page.tsx rename to apps/web/app/(auth)/signup/page.tsx diff --git a/app/(manage)/layout.tsx b/apps/web/app/(manage)/layout.tsx similarity index 100% rename from app/(manage)/layout.tsx rename to apps/web/app/(manage)/layout.tsx diff --git a/app/(manage)/manage/analytics/page.tsx b/apps/web/app/(manage)/manage/analytics/page.tsx similarity index 100% rename from app/(manage)/manage/analytics/page.tsx rename to apps/web/app/(manage)/manage/analytics/page.tsx diff --git a/app/(manage)/manage/benchmarks/benchmarks-client.tsx b/apps/web/app/(manage)/manage/benchmarks/benchmarks-client.tsx similarity index 100% rename from app/(manage)/manage/benchmarks/benchmarks-client.tsx rename to apps/web/app/(manage)/manage/benchmarks/benchmarks-client.tsx diff --git a/app/(manage)/manage/benchmarks/page.tsx b/apps/web/app/(manage)/manage/benchmarks/page.tsx similarity index 100% rename from app/(manage)/manage/benchmarks/page.tsx rename to apps/web/app/(manage)/manage/benchmarks/page.tsx diff --git a/app/(manage)/manage/comments/comments-client.tsx b/apps/web/app/(manage)/manage/comments/comments-client.tsx similarity index 100% rename from app/(manage)/manage/comments/comments-client.tsx rename to apps/web/app/(manage)/manage/comments/comments-client.tsx diff --git a/app/(manage)/manage/comments/page.tsx b/apps/web/app/(manage)/manage/comments/page.tsx similarity index 100% rename from app/(manage)/manage/comments/page.tsx rename to apps/web/app/(manage)/manage/comments/page.tsx diff --git a/app/(manage)/manage/games/games-client.tsx b/apps/web/app/(manage)/manage/games/games-client.tsx similarity index 100% rename from app/(manage)/manage/games/games-client.tsx rename to apps/web/app/(manage)/manage/games/games-client.tsx diff --git a/app/(manage)/manage/games/page.tsx b/apps/web/app/(manage)/manage/games/page.tsx similarity index 100% rename from app/(manage)/manage/games/page.tsx rename to apps/web/app/(manage)/manage/games/page.tsx diff --git a/app/(manage)/manage/hardware/hardware-client.tsx b/apps/web/app/(manage)/manage/hardware/hardware-client.tsx similarity index 100% rename from app/(manage)/manage/hardware/hardware-client.tsx rename to apps/web/app/(manage)/manage/hardware/hardware-client.tsx diff --git a/app/(manage)/manage/hardware/page.tsx b/apps/web/app/(manage)/manage/hardware/page.tsx similarity index 100% rename from app/(manage)/manage/hardware/page.tsx rename to apps/web/app/(manage)/manage/hardware/page.tsx diff --git a/app/(manage)/manage/page.tsx b/apps/web/app/(manage)/manage/page.tsx similarity index 100% rename from app/(manage)/manage/page.tsx rename to apps/web/app/(manage)/manage/page.tsx diff --git a/app/(manage)/manage/reports/page.tsx b/apps/web/app/(manage)/manage/reports/page.tsx similarity index 100% rename from app/(manage)/manage/reports/page.tsx rename to apps/web/app/(manage)/manage/reports/page.tsx diff --git a/app/(manage)/manage/reports/reports-client.tsx b/apps/web/app/(manage)/manage/reports/reports-client.tsx similarity index 100% rename from app/(manage)/manage/reports/reports-client.tsx rename to apps/web/app/(manage)/manage/reports/reports-client.tsx diff --git a/app/(manage)/manage/storage/page.tsx b/apps/web/app/(manage)/manage/storage/page.tsx similarity index 100% rename from app/(manage)/manage/storage/page.tsx rename to apps/web/app/(manage)/manage/storage/page.tsx diff --git a/app/(manage)/manage/storage/storage-client.tsx b/apps/web/app/(manage)/manage/storage/storage-client.tsx similarity index 100% rename from app/(manage)/manage/storage/storage-client.tsx rename to apps/web/app/(manage)/manage/storage/storage-client.tsx diff --git a/app/(manage)/manage/suggestions/page.tsx b/apps/web/app/(manage)/manage/suggestions/page.tsx similarity index 100% rename from app/(manage)/manage/suggestions/page.tsx rename to apps/web/app/(manage)/manage/suggestions/page.tsx diff --git a/app/(manage)/manage/suggestions/suggestions-client.tsx b/apps/web/app/(manage)/manage/suggestions/suggestions-client.tsx similarity index 100% rename from app/(manage)/manage/suggestions/suggestions-client.tsx rename to apps/web/app/(manage)/manage/suggestions/suggestions-client.tsx diff --git a/app/(manage)/manage/users/page.tsx b/apps/web/app/(manage)/manage/users/page.tsx similarity index 100% rename from app/(manage)/manage/users/page.tsx rename to apps/web/app/(manage)/manage/users/page.tsx diff --git a/app/(manage)/manage/users/users-client.tsx b/apps/web/app/(manage)/manage/users/users-client.tsx similarity index 100% rename from app/(manage)/manage/users/users-client.tsx rename to apps/web/app/(manage)/manage/users/users-client.tsx diff --git a/app/__tests__/metadata.test.ts b/apps/web/app/__tests__/metadata.test.ts similarity index 100% rename from app/__tests__/metadata.test.ts rename to apps/web/app/__tests__/metadata.test.ts diff --git a/app/__tests__/sitemap.integration.test.ts b/apps/web/app/__tests__/sitemap.integration.test.ts similarity index 100% rename from app/__tests__/sitemap.integration.test.ts rename to apps/web/app/__tests__/sitemap.integration.test.ts diff --git a/app/__tests__/sitemap.test.ts b/apps/web/app/__tests__/sitemap.test.ts similarity index 100% rename from app/__tests__/sitemap.test.ts rename to apps/web/app/__tests__/sitemap.test.ts diff --git a/app/api/[[...slugs]]/route.ts b/apps/web/app/api/[[...slugs]]/route.ts similarity index 100% rename from app/api/[[...slugs]]/route.ts rename to apps/web/app/api/[[...slugs]]/route.ts diff --git a/app/app/[steamid]/page.tsx b/apps/web/app/app/[steamid]/page.tsx similarity index 100% rename from app/app/[steamid]/page.tsx rename to apps/web/app/app/[steamid]/page.tsx diff --git a/app/app/__tests__/redirect.test.ts b/apps/web/app/app/__tests__/redirect.test.ts similarity index 100% rename from app/app/__tests__/redirect.test.ts rename to apps/web/app/app/__tests__/redirect.test.ts diff --git a/app/compare/layout.tsx b/apps/web/app/compare/layout.tsx similarity index 100% rename from app/compare/layout.tsx rename to apps/web/app/compare/layout.tsx diff --git a/app/compare/page.tsx b/apps/web/app/compare/page.tsx similarity index 100% rename from app/compare/page.tsx rename to apps/web/app/compare/page.tsx diff --git a/app/components/PlatformIcons.tsx b/apps/web/app/components/PlatformIcons.tsx similarity index 100% rename from app/components/PlatformIcons.tsx rename to apps/web/app/components/PlatformIcons.tsx diff --git a/app/contact/layout.tsx b/apps/web/app/contact/layout.tsx similarity index 100% rename from app/contact/layout.tsx rename to apps/web/app/contact/layout.tsx diff --git a/app/contact/page.tsx b/apps/web/app/contact/page.tsx similarity index 100% rename from app/contact/page.tsx rename to apps/web/app/contact/page.tsx diff --git a/app/devices/[slug]/device-detail-client.tsx b/apps/web/app/devices/[slug]/device-detail-client.tsx similarity index 100% rename from app/devices/[slug]/device-detail-client.tsx rename to apps/web/app/devices/[slug]/device-detail-client.tsx diff --git a/app/devices/[slug]/opengraph-image.tsx b/apps/web/app/devices/[slug]/opengraph-image.tsx similarity index 100% rename from app/devices/[slug]/opengraph-image.tsx rename to apps/web/app/devices/[slug]/opengraph-image.tsx diff --git a/app/devices/[slug]/page.tsx b/apps/web/app/devices/[slug]/page.tsx similarity index 100% rename from app/devices/[slug]/page.tsx rename to apps/web/app/devices/[slug]/page.tsx diff --git a/app/devices/page-client.tsx b/apps/web/app/devices/page-client.tsx similarity index 100% rename from app/devices/page-client.tsx rename to apps/web/app/devices/page-client.tsx diff --git a/app/devices/page.tsx b/apps/web/app/devices/page.tsx similarity index 100% rename from app/devices/page.tsx rename to apps/web/app/devices/page.tsx diff --git a/app/game/[id]/edit/page.tsx b/apps/web/app/game/[id]/edit/page.tsx similarity index 100% rename from app/game/[id]/edit/page.tsx rename to apps/web/app/game/[id]/edit/page.tsx diff --git a/app/game/[id]/game-page-client.tsx b/apps/web/app/game/[id]/game-page-client.tsx similarity index 100% rename from app/game/[id]/game-page-client.tsx rename to apps/web/app/game/[id]/game-page-client.tsx diff --git a/app/game/[id]/opengraph-image.tsx b/apps/web/app/game/[id]/opengraph-image.tsx similarity index 100% rename from app/game/[id]/opengraph-image.tsx rename to apps/web/app/game/[id]/opengraph-image.tsx diff --git a/app/game/[id]/page.tsx b/apps/web/app/game/[id]/page.tsx similarity index 100% rename from app/game/[id]/page.tsx rename to apps/web/app/game/[id]/page.tsx diff --git a/app/game/[id]/preset-detail-modal.tsx b/apps/web/app/game/[id]/preset-detail-modal.tsx similarity index 100% rename from app/game/[id]/preset-detail-modal.tsx rename to apps/web/app/game/[id]/preset-detail-modal.tsx diff --git a/app/game/[id]/submit/page.tsx b/apps/web/app/game/[id]/submit/page.tsx similarity index 100% rename from app/game/[id]/submit/page.tsx rename to apps/web/app/game/[id]/submit/page.tsx diff --git a/app/game/add/page.tsx b/apps/web/app/game/add/page.tsx similarity index 100% rename from app/game/add/page.tsx rename to apps/web/app/game/add/page.tsx diff --git a/app/games/games-page-client.tsx b/apps/web/app/games/games-page-client.tsx similarity index 100% rename from app/games/games-page-client.tsx rename to apps/web/app/games/games-page-client.tsx diff --git a/app/games/page.tsx b/apps/web/app/games/page.tsx similarity index 100% rename from app/games/page.tsx rename to apps/web/app/games/page.tsx diff --git a/app/globals.css b/apps/web/app/globals.css similarity index 100% rename from app/globals.css rename to apps/web/app/globals.css diff --git a/app/icon.png b/apps/web/app/icon.png similarity index 100% rename from app/icon.png rename to apps/web/app/icon.png diff --git a/app/layout.tsx b/apps/web/app/layout.tsx similarity index 100% rename from app/layout.tsx rename to apps/web/app/layout.tsx diff --git a/app/manifest.ts b/apps/web/app/manifest.ts similarity index 100% rename from app/manifest.ts rename to apps/web/app/manifest.ts diff --git a/app/not-found.tsx b/apps/web/app/not-found.tsx similarity index 100% rename from app/not-found.tsx rename to apps/web/app/not-found.tsx diff --git a/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx similarity index 100% rename from app/opengraph-image.tsx rename to apps/web/app/opengraph-image.tsx diff --git a/app/page.tsx b/apps/web/app/page.tsx similarity index 100% rename from app/page.tsx rename to apps/web/app/page.tsx diff --git a/app/profile/[id]/page.tsx b/apps/web/app/profile/[id]/page.tsx similarity index 100% rename from app/profile/[id]/page.tsx rename to apps/web/app/profile/[id]/page.tsx diff --git a/app/profile/[id]/profile-page-client.tsx b/apps/web/app/profile/[id]/profile-page-client.tsx similarity index 100% rename from app/profile/[id]/profile-page-client.tsx rename to apps/web/app/profile/[id]/profile-page-client.tsx diff --git a/app/profile/layout.tsx b/apps/web/app/profile/layout.tsx similarity index 100% rename from app/profile/layout.tsx rename to apps/web/app/profile/layout.tsx diff --git a/app/profile/page.tsx b/apps/web/app/profile/page.tsx similarity index 100% rename from app/profile/page.tsx rename to apps/web/app/profile/page.tsx diff --git a/app/robots.ts b/apps/web/app/robots.ts similarity index 100% rename from app/robots.ts rename to apps/web/app/robots.ts diff --git a/app/search/layout.tsx b/apps/web/app/search/layout.tsx similarity index 100% rename from app/search/layout.tsx rename to apps/web/app/search/layout.tsx diff --git a/app/search/page.tsx b/apps/web/app/search/page.tsx similarity index 100% rename from app/search/page.tsx rename to apps/web/app/search/page.tsx diff --git a/app/sitemap.ts b/apps/web/app/sitemap.ts similarity index 100% rename from app/sitemap.ts rename to apps/web/app/sitemap.ts diff --git a/app/sw.ts b/apps/web/app/sw.ts similarity index 100% rename from app/sw.ts rename to apps/web/app/sw.ts diff --git a/app/test-version-fetchers/page.tsx b/apps/web/app/test-version-fetchers/page.tsx similarity index 100% rename from app/test-version-fetchers/page.tsx rename to apps/web/app/test-version-fetchers/page.tsx diff --git a/app/twitter-image.tsx b/apps/web/app/twitter-image.tsx similarity index 100% rename from app/twitter-image.tsx rename to apps/web/app/twitter-image.tsx diff --git a/app/updates/[slug]/page.tsx b/apps/web/app/updates/[slug]/page.tsx similarity index 100% rename from app/updates/[slug]/page.tsx rename to apps/web/app/updates/[slug]/page.tsx diff --git a/app/updates/page.tsx b/apps/web/app/updates/page.tsx similarity index 100% rename from app/updates/page.tsx rename to apps/web/app/updates/page.tsx diff --git a/components/anti-cheat-badge.tsx b/apps/web/components/anti-cheat-badge.tsx similarity index 100% rename from components/anti-cheat-badge.tsx rename to apps/web/components/anti-cheat-badge.tsx diff --git a/components/auth/__tests__/placeholder.test.tsx b/apps/web/components/auth/__tests__/placeholder.test.tsx similarity index 100% rename from components/auth/__tests__/placeholder.test.tsx rename to apps/web/components/auth/__tests__/placeholder.test.tsx diff --git a/components/auth/forgot-password-form.tsx b/apps/web/components/auth/forgot-password-form.tsx similarity index 100% rename from components/auth/forgot-password-form.tsx rename to apps/web/components/auth/forgot-password-form.tsx diff --git a/components/auth/login-form.tsx b/apps/web/components/auth/login-form.tsx similarity index 100% rename from components/auth/login-form.tsx rename to apps/web/components/auth/login-form.tsx diff --git a/components/auth/otp-input.tsx b/apps/web/components/auth/otp-input.tsx similarity index 100% rename from components/auth/otp-input.tsx rename to apps/web/components/auth/otp-input.tsx diff --git a/components/auth/otp-verification-step.tsx b/apps/web/components/auth/otp-verification-step.tsx similarity index 100% rename from components/auth/otp-verification-step.tsx rename to apps/web/components/auth/otp-verification-step.tsx diff --git a/components/auth/passkey-setup-step.tsx b/apps/web/components/auth/passkey-setup-step.tsx similarity index 100% rename from components/auth/passkey-setup-step.tsx rename to apps/web/components/auth/passkey-setup-step.tsx diff --git a/components/auth/password-strength.tsx b/apps/web/components/auth/password-strength.tsx similarity index 100% rename from components/auth/password-strength.tsx rename to apps/web/components/auth/password-strength.tsx diff --git a/components/auth/reset-password-form.tsx b/apps/web/components/auth/reset-password-form.tsx similarity index 100% rename from components/auth/reset-password-form.tsx rename to apps/web/components/auth/reset-password-form.tsx diff --git a/components/auth/signup-form-step.tsx b/apps/web/components/auth/signup-form-step.tsx similarity index 100% rename from components/auth/signup-form-step.tsx rename to apps/web/components/auth/signup-form-step.tsx diff --git a/components/auth/signup-wizard.tsx b/apps/web/components/auth/signup-wizard.tsx similarity index 100% rename from components/auth/signup-wizard.tsx rename to apps/web/components/auth/signup-wizard.tsx diff --git a/components/auth/social-buttons.tsx b/apps/web/components/auth/social-buttons.tsx similarity index 100% rename from components/auth/social-buttons.tsx rename to apps/web/components/auth/social-buttons.tsx diff --git a/components/auth/step-indicator.tsx b/apps/web/components/auth/step-indicator.tsx similarity index 100% rename from components/auth/step-indicator.tsx rename to apps/web/components/auth/step-indicator.tsx diff --git a/components/auth/turnstile-widget.tsx b/apps/web/components/auth/turnstile-widget.tsx similarity index 100% rename from components/auth/turnstile-widget.tsx rename to apps/web/components/auth/turnstile-widget.tsx diff --git a/components/charts/BatteryLifeChart.tsx b/apps/web/components/charts/BatteryLifeChart.tsx similarity index 100% rename from components/charts/BatteryLifeChart.tsx rename to apps/web/components/charts/BatteryLifeChart.tsx diff --git a/components/charts/DeviceDonut.tsx b/apps/web/components/charts/DeviceDonut.tsx similarity index 100% rename from components/charts/DeviceDonut.tsx rename to apps/web/components/charts/DeviceDonut.tsx diff --git a/components/charts/EChartWrapper.tsx b/apps/web/components/charts/EChartWrapper.tsx similarity index 100% rename from components/charts/EChartWrapper.tsx rename to apps/web/components/charts/EChartWrapper.tsx diff --git a/components/charts/FpsBoxplot.tsx b/apps/web/components/charts/FpsBoxplot.tsx similarity index 100% rename from components/charts/FpsBoxplot.tsx rename to apps/web/components/charts/FpsBoxplot.tsx diff --git a/components/charts/FpsRangeChart.tsx b/apps/web/components/charts/FpsRangeChart.tsx similarity index 100% rename from components/charts/FpsRangeChart.tsx rename to apps/web/components/charts/FpsRangeChart.tsx diff --git a/components/charts/HistoricalAreaChart.tsx b/apps/web/components/charts/HistoricalAreaChart.tsx similarity index 100% rename from components/charts/HistoricalAreaChart.tsx rename to apps/web/components/charts/HistoricalAreaChart.tsx diff --git a/components/charts/PerformanceTierChart.tsx b/apps/web/components/charts/PerformanceTierChart.tsx similarity index 100% rename from components/charts/PerformanceTierChart.tsx rename to apps/web/components/charts/PerformanceTierChart.tsx diff --git a/components/charts/StabilityScatterChart.tsx b/apps/web/components/charts/StabilityScatterChart.tsx similarity index 100% rename from components/charts/StabilityScatterChart.tsx rename to apps/web/components/charts/StabilityScatterChart.tsx diff --git a/components/charts/UpscalerBarChart.tsx b/apps/web/components/charts/UpscalerBarChart.tsx similarity index 100% rename from components/charts/UpscalerBarChart.tsx rename to apps/web/components/charts/UpscalerBarChart.tsx diff --git a/components/comments/comment-item.tsx b/apps/web/components/comments/comment-item.tsx similarity index 100% rename from components/comments/comment-item.tsx rename to apps/web/components/comments/comment-item.tsx diff --git a/components/comments/comment-section.tsx b/apps/web/components/comments/comment-section.tsx similarity index 100% rename from components/comments/comment-section.tsx rename to apps/web/components/comments/comment-section.tsx diff --git a/components/community-suggestion-form.tsx b/apps/web/components/community-suggestion-form.tsx similarity index 100% rename from components/community-suggestion-form.tsx rename to apps/web/components/community-suggestion-form.tsx diff --git a/components/compare/fps-comparison-chart.tsx b/apps/web/components/compare/fps-comparison-chart.tsx similarity index 100% rename from components/compare/fps-comparison-chart.tsx rename to apps/web/components/compare/fps-comparison-chart.tsx diff --git a/components/compare/game-selector.tsx b/apps/web/components/compare/game-selector.tsx similarity index 100% rename from components/compare/game-selector.tsx rename to apps/web/components/compare/game-selector.tsx diff --git a/components/compare/stability-radar.tsx b/apps/web/components/compare/stability-radar.tsx similarity index 100% rename from components/compare/stability-radar.tsx rename to apps/web/components/compare/stability-radar.tsx diff --git a/components/compare/stats-comparison.tsx b/apps/web/components/compare/stats-comparison.tsx similarity index 100% rename from components/compare/stats-comparison.tsx rename to apps/web/components/compare/stats-comparison.tsx diff --git a/components/filter-drawer.tsx b/apps/web/components/filter-drawer.tsx similarity index 100% rename from components/filter-drawer.tsx rename to apps/web/components/filter-drawer.tsx diff --git a/components/manage/analytics-client.tsx b/apps/web/components/manage/analytics-client.tsx similarity index 100% rename from components/manage/analytics-client.tsx rename to apps/web/components/manage/analytics-client.tsx diff --git a/components/manage/dashboard-charts.tsx b/apps/web/components/manage/dashboard-charts.tsx similarity index 100% rename from components/manage/dashboard-charts.tsx rename to apps/web/components/manage/dashboard-charts.tsx diff --git a/components/manage/dashboard-overview.tsx b/apps/web/components/manage/dashboard-overview.tsx similarity index 100% rename from components/manage/dashboard-overview.tsx rename to apps/web/components/manage/dashboard-overview.tsx diff --git a/components/manage/manage-sidebar.tsx b/apps/web/components/manage/manage-sidebar.tsx similarity index 100% rename from components/manage/manage-sidebar.tsx rename to apps/web/components/manage/manage-sidebar.tsx diff --git a/components/navbar.tsx b/apps/web/components/navbar.tsx similarity index 100% rename from components/navbar.tsx rename to apps/web/components/navbar.tsx diff --git a/components/playability-badge.tsx b/apps/web/components/playability-badge.tsx similarity index 100% rename from components/playability-badge.tsx rename to apps/web/components/playability-badge.tsx diff --git a/components/profile/contribution-list.tsx b/apps/web/components/profile/contribution-list.tsx similarity index 100% rename from components/profile/contribution-list.tsx rename to apps/web/components/profile/contribution-list.tsx diff --git a/components/profile/profile-header.tsx b/apps/web/components/profile/profile-header.tsx similarity index 100% rename from components/profile/profile-header.tsx rename to apps/web/components/profile/profile-header.tsx diff --git a/components/profile/profile-photo-upload.tsx b/apps/web/components/profile/profile-photo-upload.tsx similarity index 100% rename from components/profile/profile-photo-upload.tsx rename to apps/web/components/profile/profile-photo-upload.tsx diff --git a/components/profile/settings-accounts-tab.tsx b/apps/web/components/profile/settings-accounts-tab.tsx similarity index 100% rename from components/profile/settings-accounts-tab.tsx rename to apps/web/components/profile/settings-accounts-tab.tsx diff --git a/components/profile/settings-container.tsx b/apps/web/components/profile/settings-container.tsx similarity index 100% rename from components/profile/settings-container.tsx rename to apps/web/components/profile/settings-container.tsx diff --git a/components/profile/settings-profile-tab.tsx b/apps/web/components/profile/settings-profile-tab.tsx similarity index 100% rename from components/profile/settings-profile-tab.tsx rename to apps/web/components/profile/settings-profile-tab.tsx diff --git a/components/profile/settings-security-tab.tsx b/apps/web/components/profile/settings-security-tab.tsx similarity index 100% rename from components/profile/settings-security-tab.tsx rename to apps/web/components/profile/settings-security-tab.tsx diff --git a/components/profile/stats-row.tsx b/apps/web/components/profile/stats-row.tsx similarity index 100% rename from components/profile/stats-row.tsx rename to apps/web/components/profile/stats-row.tsx diff --git a/components/saved-filters.tsx b/apps/web/components/saved-filters.tsx similarity index 100% rename from components/saved-filters.tsx rename to apps/web/components/saved-filters.tsx diff --git a/components/saved-games/bookmark-button.tsx b/apps/web/components/saved-games/bookmark-button.tsx similarity index 100% rename from components/saved-games/bookmark-button.tsx rename to apps/web/components/saved-games/bookmark-button.tsx diff --git a/components/saved-games/saved-games-grid.tsx b/apps/web/components/saved-games/saved-games-grid.tsx similarity index 100% rename from components/saved-games/saved-games-grid.tsx rename to apps/web/components/saved-games/saved-games-grid.tsx diff --git a/components/steam-reviews.tsx b/apps/web/components/steam-reviews.tsx similarity index 100% rename from components/steam-reviews.tsx rename to apps/web/components/steam-reviews.tsx diff --git a/components/tiptap-editor.tsx b/apps/web/components/tiptap-editor.tsx similarity index 100% rename from components/tiptap-editor.tsx rename to apps/web/components/tiptap-editor.tsx diff --git a/components/tiptap-renderer.tsx b/apps/web/components/tiptap-renderer.tsx similarity index 100% rename from components/tiptap-renderer.tsx rename to apps/web/components/tiptap-renderer.tsx diff --git a/components/ui/modal/confirm-dialog.tsx b/apps/web/components/ui/modal/confirm-dialog.tsx similarity index 100% rename from components/ui/modal/confirm-dialog.tsx rename to apps/web/components/ui/modal/confirm-dialog.tsx diff --git a/components/ui/modal/index.ts b/apps/web/components/ui/modal/index.ts similarity index 100% rename from components/ui/modal/index.ts rename to apps/web/components/ui/modal/index.ts diff --git a/components/ui/modal/modal.tsx b/apps/web/components/ui/modal/modal.tsx similarity index 100% rename from components/ui/modal/modal.tsx rename to apps/web/components/ui/modal/modal.tsx diff --git a/components/ui/screenshot-lightbox.tsx b/apps/web/components/ui/screenshot-lightbox.tsx similarity index 100% rename from components/ui/screenshot-lightbox.tsx rename to apps/web/components/ui/screenshot-lightbox.tsx diff --git a/components/updates/chapter-nav.tsx b/apps/web/components/updates/chapter-nav.tsx similarity index 100% rename from components/updates/chapter-nav.tsx rename to apps/web/components/updates/chapter-nav.tsx diff --git a/components/updates/reading-progress-bar.tsx b/apps/web/components/updates/reading-progress-bar.tsx similarity index 100% rename from components/updates/reading-progress-bar.tsx rename to apps/web/components/updates/reading-progress-bar.tsx diff --git a/components/updates/update-card.tsx b/apps/web/components/updates/update-card.tsx similarity index 100% rename from components/updates/update-card.tsx rename to apps/web/components/updates/update-card.tsx diff --git a/components/updates/update-viewer.tsx b/apps/web/components/updates/update-viewer.tsx similarity index 100% rename from components/updates/update-viewer.tsx rename to apps/web/components/updates/update-viewer.tsx diff --git a/components/wizard/game-entry-wizard.tsx b/apps/web/components/wizard/game-entry-wizard.tsx similarity index 100% rename from components/wizard/game-entry-wizard.tsx rename to apps/web/components/wizard/game-entry-wizard.tsx diff --git a/components/wizard/non-steam-edit-form.tsx b/apps/web/components/wizard/non-steam-edit-form.tsx similarity index 100% rename from components/wizard/non-steam-edit-form.tsx rename to apps/web/components/wizard/non-steam-edit-form.tsx diff --git a/components/wizard/non-steam-wizard.tsx b/apps/web/components/wizard/non-steam-wizard.tsx similarity index 100% rename from components/wizard/non-steam-wizard.tsx rename to apps/web/components/wizard/non-steam-wizard.tsx diff --git a/components/wizard/settings-editor.tsx b/apps/web/components/wizard/settings-editor.tsx similarity index 100% rename from components/wizard/settings-editor.tsx rename to apps/web/components/wizard/settings-editor.tsx diff --git a/components/wizard/step-indicator.tsx b/apps/web/components/wizard/step-indicator.tsx similarity index 100% rename from components/wizard/step-indicator.tsx rename to apps/web/components/wizard/step-indicator.tsx diff --git a/components/wizard/steps/anti-cheat-step.tsx b/apps/web/components/wizard/steps/anti-cheat-step.tsx similarity index 100% rename from components/wizard/steps/anti-cheat-step.tsx rename to apps/web/components/wizard/steps/anti-cheat-step.tsx diff --git a/components/wizard/steps/environment-step.tsx b/apps/web/components/wizard/steps/environment-step.tsx similarity index 100% rename from components/wizard/steps/environment-step.tsx rename to apps/web/components/wizard/steps/environment-step.tsx diff --git a/components/wizard/steps/hardware-step.tsx b/apps/web/components/wizard/steps/hardware-step.tsx similarity index 100% rename from components/wizard/steps/hardware-step.tsx rename to apps/web/components/wizard/steps/hardware-step.tsx diff --git a/components/wizard/steps/index.ts b/apps/web/components/wizard/steps/index.ts similarity index 100% rename from components/wizard/steps/index.ts rename to apps/web/components/wizard/steps/index.ts diff --git a/components/wizard/steps/non-steam-basic-info-step.tsx b/apps/web/components/wizard/steps/non-steam-basic-info-step.tsx similarity index 100% rename from components/wizard/steps/non-steam-basic-info-step.tsx rename to apps/web/components/wizard/steps/non-steam-basic-info-step.tsx diff --git a/components/wizard/steps/non-steam-image-step.tsx b/apps/web/components/wizard/steps/non-steam-image-step.tsx similarity index 100% rename from components/wizard/steps/non-steam-image-step.tsx rename to apps/web/components/wizard/steps/non-steam-image-step.tsx diff --git a/components/wizard/steps/non-steam-platform-step.tsx b/apps/web/components/wizard/steps/non-steam-platform-step.tsx similarity index 100% rename from components/wizard/steps/non-steam-platform-step.tsx rename to apps/web/components/wizard/steps/non-steam-platform-step.tsx diff --git a/components/wizard/steps/non-steam-review-step.tsx b/apps/web/components/wizard/steps/non-steam-review-step.tsx similarity index 100% rename from components/wizard/steps/non-steam-review-step.tsx rename to apps/web/components/wizard/steps/non-steam-review-step.tsx diff --git a/components/wizard/steps/performance-step.tsx b/apps/web/components/wizard/steps/performance-step.tsx similarity index 100% rename from components/wizard/steps/performance-step.tsx rename to apps/web/components/wizard/steps/performance-step.tsx diff --git a/components/wizard/steps/review-step.tsx b/apps/web/components/wizard/steps/review-step.tsx similarity index 100% rename from components/wizard/steps/review-step.tsx rename to apps/web/components/wizard/steps/review-step.tsx diff --git a/components/wizard/steps/settings-step.tsx b/apps/web/components/wizard/steps/settings-step.tsx similarity index 100% rename from components/wizard/steps/settings-step.tsx rename to apps/web/components/wizard/steps/settings-step.tsx diff --git a/components/wizard/steps/setup-step.tsx b/apps/web/components/wizard/steps/setup-step.tsx similarity index 100% rename from components/wizard/steps/setup-step.tsx rename to apps/web/components/wizard/steps/setup-step.tsx diff --git a/content/updates/2026-04-30-v2026.0.95.md b/apps/web/content/updates/2026-04-30-v2026.0.95.md similarity index 100% rename from content/updates/2026-04-30-v2026.0.95.md rename to apps/web/content/updates/2026-04-30-v2026.0.95.md diff --git a/content/updates/2026-05-01-v2026.0.96.md b/apps/web/content/updates/2026-05-01-v2026.0.96.md similarity index 100% rename from content/updates/2026-05-01-v2026.0.96.md rename to apps/web/content/updates/2026-05-01-v2026.0.96.md diff --git a/content/updates/2026-05-06-v2026.0.97.md b/apps/web/content/updates/2026-05-06-v2026.0.97.md similarity index 100% rename from content/updates/2026-05-06-v2026.0.97.md rename to apps/web/content/updates/2026-05-06-v2026.0.97.md diff --git a/content/updates/2026-05-09-v2026.0.97.1.md b/apps/web/content/updates/2026-05-09-v2026.0.97.1.md similarity index 100% rename from content/updates/2026-05-09-v2026.0.97.1.md rename to apps/web/content/updates/2026-05-09-v2026.0.97.1.md diff --git a/content/updates/2026-05-09-v2026.0.98.md b/apps/web/content/updates/2026-05-09-v2026.0.98.md similarity index 100% rename from content/updates/2026-05-09-v2026.0.98.md rename to apps/web/content/updates/2026-05-09-v2026.0.98.md diff --git a/content/updates/2026-05-09-v2026.0.99.md b/apps/web/content/updates/2026-05-09-v2026.0.99.md similarity index 100% rename from content/updates/2026-05-09-v2026.0.99.md rename to apps/web/content/updates/2026-05-09-v2026.0.99.md diff --git a/content/updates/2026-05-10-v2026.0.100.md b/apps/web/content/updates/2026-05-10-v2026.0.100.md similarity index 100% rename from content/updates/2026-05-10-v2026.0.100.md rename to apps/web/content/updates/2026-05-10-v2026.0.100.md diff --git a/content/updates/2026-05-14-v2026.0.101.md b/apps/web/content/updates/2026-05-14-v2026.0.101.md similarity index 100% rename from content/updates/2026-05-14-v2026.0.101.md rename to apps/web/content/updates/2026-05-14-v2026.0.101.md diff --git a/content/updates/2026-05-16-v2026.0.102.md b/apps/web/content/updates/2026-05-16-v2026.0.102.md similarity index 100% rename from content/updates/2026-05-16-v2026.0.102.md rename to apps/web/content/updates/2026-05-16-v2026.0.102.md diff --git a/content/updates/2026-05-20-v2026.2.0.md b/apps/web/content/updates/2026-05-20-v2026.2.0.md similarity index 100% rename from content/updates/2026-05-20-v2026.2.0.md rename to apps/web/content/updates/2026-05-20-v2026.2.0.md diff --git a/content/updates/2026-05-25-v2026.2.1.md b/apps/web/content/updates/2026-05-25-v2026.2.1.md similarity index 100% rename from content/updates/2026-05-25-v2026.2.1.md rename to apps/web/content/updates/2026-05-25-v2026.2.1.md diff --git a/content/updates/2026-05-27-v2026.2.2.md b/apps/web/content/updates/2026-05-27-v2026.2.2.md similarity index 100% rename from content/updates/2026-05-27-v2026.2.2.md rename to apps/web/content/updates/2026-05-27-v2026.2.2.md diff --git a/drizzle.config.ts b/apps/web/drizzle.config.ts similarity index 100% rename from drizzle.config.ts rename to apps/web/drizzle.config.ts diff --git a/drizzle/0000_crazy_nick_fury.sql b/apps/web/drizzle/0000_crazy_nick_fury.sql similarity index 100% rename from drizzle/0000_crazy_nick_fury.sql rename to apps/web/drizzle/0000_crazy_nick_fury.sql diff --git a/drizzle/0001_big_smiling_tiger.sql b/apps/web/drizzle/0001_big_smiling_tiger.sql similarity index 100% rename from drizzle/0001_big_smiling_tiger.sql rename to apps/web/drizzle/0001_big_smiling_tiger.sql diff --git a/drizzle/0002_square_slayback.sql b/apps/web/drizzle/0002_square_slayback.sql similarity index 100% rename from drizzle/0002_square_slayback.sql rename to apps/web/drizzle/0002_square_slayback.sql diff --git a/drizzle/0004_aspiring_meggan.sql b/apps/web/drizzle/0004_aspiring_meggan.sql similarity index 100% rename from drizzle/0004_aspiring_meggan.sql rename to apps/web/drizzle/0004_aspiring_meggan.sql diff --git a/drizzle/0005_add_preset_performance_link.sql b/apps/web/drizzle/0005_add_preset_performance_link.sql similarity index 100% rename from drizzle/0005_add_preset_performance_link.sql rename to apps/web/drizzle/0005_add_preset_performance_link.sql diff --git a/drizzle/0005_brief_marvel_zombies.sql b/apps/web/drizzle/0005_brief_marvel_zombies.sql similarity index 100% rename from drizzle/0005_brief_marvel_zombies.sql rename to apps/web/drizzle/0005_brief_marvel_zombies.sql diff --git a/drizzle/0006_upscaler_battery_custom_system.sql b/apps/web/drizzle/0006_upscaler_battery_custom_system.sql similarity index 100% rename from drizzle/0006_upscaler_battery_custom_system.sql rename to apps/web/drizzle/0006_upscaler_battery_custom_system.sql diff --git a/drizzle/0007_mighty_electro.sql b/apps/web/drizzle/0007_mighty_electro.sql similarity index 100% rename from drizzle/0007_mighty_electro.sql rename to apps/web/drizzle/0007_mighty_electro.sql diff --git a/drizzle/0008_slim_jubilee.sql b/apps/web/drizzle/0008_slim_jubilee.sql similarity index 100% rename from drizzle/0008_slim_jubilee.sql rename to apps/web/drizzle/0008_slim_jubilee.sql diff --git a/drizzle/0009_flawless_meltdown.sql b/apps/web/drizzle/0009_flawless_meltdown.sql similarity index 100% rename from drizzle/0009_flawless_meltdown.sql rename to apps/web/drizzle/0009_flawless_meltdown.sql diff --git a/drizzle/0010_soft_wilson_fisk.sql b/apps/web/drizzle/0010_soft_wilson_fisk.sql similarity index 100% rename from drizzle/0010_soft_wilson_fisk.sql rename to apps/web/drizzle/0010_soft_wilson_fisk.sql diff --git a/drizzle/0011_rename-handled-to-handheld-add-image.sql b/apps/web/drizzle/0011_rename-handled-to-handheld-add-image.sql similarity index 100% rename from drizzle/0011_rename-handled-to-handheld-add-image.sql rename to apps/web/drizzle/0011_rename-handled-to-handheld-add-image.sql diff --git a/drizzle/0011_talented_bloodstorm.sql b/apps/web/drizzle/0011_talented_bloodstorm.sql similarity index 100% rename from drizzle/0011_talented_bloodstorm.sql rename to apps/web/drizzle/0011_talented_bloodstorm.sql diff --git a/drizzle/0012_clever_thunderbolt_ross.sql b/apps/web/drizzle/0012_clever_thunderbolt_ross.sql similarity index 100% rename from drizzle/0012_clever_thunderbolt_ross.sql rename to apps/web/drizzle/0012_clever_thunderbolt_ross.sql diff --git a/drizzle/0013_supreme_purple_man.sql b/apps/web/drizzle/0013_supreme_purple_man.sql similarity index 100% rename from drizzle/0013_supreme_purple_man.sql rename to apps/web/drizzle/0013_supreme_purple_man.sql diff --git a/drizzle/0014_purple_firebird.sql b/apps/web/drizzle/0014_purple_firebird.sql similarity index 100% rename from drizzle/0014_purple_firebird.sql rename to apps/web/drizzle/0014_purple_firebird.sql diff --git a/drizzle/0015_nappy_the_phantom.sql b/apps/web/drizzle/0015_nappy_the_phantom.sql similarity index 100% rename from drizzle/0015_nappy_the_phantom.sql rename to apps/web/drizzle/0015_nappy_the_phantom.sql diff --git a/drizzle/0016_zippy_eternals.sql b/apps/web/drizzle/0016_zippy_eternals.sql similarity index 100% rename from drizzle/0016_zippy_eternals.sql rename to apps/web/drizzle/0016_zippy_eternals.sql diff --git a/drizzle/0017_polite_speed_demon.sql b/apps/web/drizzle/0017_polite_speed_demon.sql similarity index 100% rename from drizzle/0017_polite_speed_demon.sql rename to apps/web/drizzle/0017_polite_speed_demon.sql diff --git a/drizzle/0018_slimy_proemial_gods.sql b/apps/web/drizzle/0018_slimy_proemial_gods.sql similarity index 100% rename from drizzle/0018_slimy_proemial_gods.sql rename to apps/web/drizzle/0018_slimy_proemial_gods.sql diff --git a/drizzle/0019_tiresome_genesis.sql b/apps/web/drizzle/0019_tiresome_genesis.sql similarity index 100% rename from drizzle/0019_tiresome_genesis.sql rename to apps/web/drizzle/0019_tiresome_genesis.sql diff --git a/drizzle/0020_flaky_lilith.sql b/apps/web/drizzle/0020_flaky_lilith.sql similarity index 100% rename from drizzle/0020_flaky_lilith.sql rename to apps/web/drizzle/0020_flaky_lilith.sql diff --git a/drizzle/0021_demonic_texas_twister.sql b/apps/web/drizzle/0021_demonic_texas_twister.sql similarity index 100% rename from drizzle/0021_demonic_texas_twister.sql rename to apps/web/drizzle/0021_demonic_texas_twister.sql diff --git a/drizzle/0022_married_zeigeist.sql b/apps/web/drizzle/0022_married_zeigeist.sql similarity index 100% rename from drizzle/0022_married_zeigeist.sql rename to apps/web/drizzle/0022_married_zeigeist.sql diff --git a/drizzle/0023_married_avengers.sql b/apps/web/drizzle/0023_married_avengers.sql similarity index 100% rename from drizzle/0023_married_avengers.sql rename to apps/web/drizzle/0023_married_avengers.sql diff --git a/drizzle/0024_violet_psynapse.sql b/apps/web/drizzle/0024_violet_psynapse.sql similarity index 100% rename from drizzle/0024_violet_psynapse.sql rename to apps/web/drizzle/0024_violet_psynapse.sql diff --git a/drizzle/0025_flowery_molten_man.sql b/apps/web/drizzle/0025_flowery_molten_man.sql similarity index 100% rename from drizzle/0025_flowery_molten_man.sql rename to apps/web/drizzle/0025_flowery_molten_man.sql diff --git a/drizzle/0026_polite_drax.sql b/apps/web/drizzle/0026_polite_drax.sql similarity index 100% rename from drizzle/0026_polite_drax.sql rename to apps/web/drizzle/0026_polite_drax.sql diff --git a/drizzle/meta/0000_snapshot.json b/apps/web/drizzle/meta/0000_snapshot.json similarity index 100% rename from drizzle/meta/0000_snapshot.json rename to apps/web/drizzle/meta/0000_snapshot.json diff --git a/drizzle/meta/0001_snapshot.json b/apps/web/drizzle/meta/0001_snapshot.json similarity index 100% rename from drizzle/meta/0001_snapshot.json rename to apps/web/drizzle/meta/0001_snapshot.json diff --git a/drizzle/meta/0002_snapshot.json b/apps/web/drizzle/meta/0002_snapshot.json similarity index 100% rename from drizzle/meta/0002_snapshot.json rename to apps/web/drizzle/meta/0002_snapshot.json diff --git a/drizzle/meta/0005_snapshot.json b/apps/web/drizzle/meta/0005_snapshot.json similarity index 100% rename from drizzle/meta/0005_snapshot.json rename to apps/web/drizzle/meta/0005_snapshot.json diff --git a/drizzle/meta/0007_snapshot.json b/apps/web/drizzle/meta/0007_snapshot.json similarity index 100% rename from drizzle/meta/0007_snapshot.json rename to apps/web/drizzle/meta/0007_snapshot.json diff --git a/drizzle/meta/0008_snapshot.json b/apps/web/drizzle/meta/0008_snapshot.json similarity index 100% rename from drizzle/meta/0008_snapshot.json rename to apps/web/drizzle/meta/0008_snapshot.json diff --git a/drizzle/meta/0009_snapshot.json b/apps/web/drizzle/meta/0009_snapshot.json similarity index 100% rename from drizzle/meta/0009_snapshot.json rename to apps/web/drizzle/meta/0009_snapshot.json diff --git a/drizzle/meta/0010_snapshot.json b/apps/web/drizzle/meta/0010_snapshot.json similarity index 100% rename from drizzle/meta/0010_snapshot.json rename to apps/web/drizzle/meta/0010_snapshot.json diff --git a/drizzle/meta/0011_snapshot.json b/apps/web/drizzle/meta/0011_snapshot.json similarity index 100% rename from drizzle/meta/0011_snapshot.json rename to apps/web/drizzle/meta/0011_snapshot.json diff --git a/drizzle/meta/0012_snapshot.json b/apps/web/drizzle/meta/0012_snapshot.json similarity index 100% rename from drizzle/meta/0012_snapshot.json rename to apps/web/drizzle/meta/0012_snapshot.json diff --git a/drizzle/meta/0013_snapshot.json b/apps/web/drizzle/meta/0013_snapshot.json similarity index 100% rename from drizzle/meta/0013_snapshot.json rename to apps/web/drizzle/meta/0013_snapshot.json diff --git a/drizzle/meta/0014_snapshot.json b/apps/web/drizzle/meta/0014_snapshot.json similarity index 100% rename from drizzle/meta/0014_snapshot.json rename to apps/web/drizzle/meta/0014_snapshot.json diff --git a/drizzle/meta/0015_snapshot.json b/apps/web/drizzle/meta/0015_snapshot.json similarity index 100% rename from drizzle/meta/0015_snapshot.json rename to apps/web/drizzle/meta/0015_snapshot.json diff --git a/drizzle/meta/0016_snapshot.json b/apps/web/drizzle/meta/0016_snapshot.json similarity index 100% rename from drizzle/meta/0016_snapshot.json rename to apps/web/drizzle/meta/0016_snapshot.json diff --git a/drizzle/meta/0017_snapshot.json b/apps/web/drizzle/meta/0017_snapshot.json similarity index 100% rename from drizzle/meta/0017_snapshot.json rename to apps/web/drizzle/meta/0017_snapshot.json diff --git a/drizzle/meta/0018_snapshot.json b/apps/web/drizzle/meta/0018_snapshot.json similarity index 100% rename from drizzle/meta/0018_snapshot.json rename to apps/web/drizzle/meta/0018_snapshot.json diff --git a/drizzle/meta/0019_snapshot.json b/apps/web/drizzle/meta/0019_snapshot.json similarity index 100% rename from drizzle/meta/0019_snapshot.json rename to apps/web/drizzle/meta/0019_snapshot.json diff --git a/drizzle/meta/0020_snapshot.json b/apps/web/drizzle/meta/0020_snapshot.json similarity index 100% rename from drizzle/meta/0020_snapshot.json rename to apps/web/drizzle/meta/0020_snapshot.json diff --git a/drizzle/meta/0021_snapshot.json b/apps/web/drizzle/meta/0021_snapshot.json similarity index 100% rename from drizzle/meta/0021_snapshot.json rename to apps/web/drizzle/meta/0021_snapshot.json diff --git a/drizzle/meta/0022_snapshot.json b/apps/web/drizzle/meta/0022_snapshot.json similarity index 100% rename from drizzle/meta/0022_snapshot.json rename to apps/web/drizzle/meta/0022_snapshot.json diff --git a/drizzle/meta/0023_snapshot.json b/apps/web/drizzle/meta/0023_snapshot.json similarity index 100% rename from drizzle/meta/0023_snapshot.json rename to apps/web/drizzle/meta/0023_snapshot.json diff --git a/drizzle/meta/0024_snapshot.json b/apps/web/drizzle/meta/0024_snapshot.json similarity index 100% rename from drizzle/meta/0024_snapshot.json rename to apps/web/drizzle/meta/0024_snapshot.json diff --git a/drizzle/meta/0025_snapshot.json b/apps/web/drizzle/meta/0025_snapshot.json similarity index 100% rename from drizzle/meta/0025_snapshot.json rename to apps/web/drizzle/meta/0025_snapshot.json diff --git a/drizzle/meta/0026_snapshot.json b/apps/web/drizzle/meta/0026_snapshot.json similarity index 100% rename from drizzle/meta/0026_snapshot.json rename to apps/web/drizzle/meta/0026_snapshot.json diff --git a/drizzle/meta/_journal.json b/apps/web/drizzle/meta/_journal.json similarity index 100% rename from drizzle/meta/_journal.json rename to apps/web/drizzle/meta/_journal.json diff --git a/eslint.config.mjs b/apps/web/eslint.config.mjs similarity index 100% rename from eslint.config.mjs rename to apps/web/eslint.config.mjs diff --git a/lib/__tests__/sitemap-utils.test.ts b/apps/web/lib/__tests__/sitemap-utils.test.ts similarity index 100% rename from lib/__tests__/sitemap-utils.test.ts rename to apps/web/lib/__tests__/sitemap-utils.test.ts diff --git a/lib/__tests__/version-bump.test.ts b/apps/web/lib/__tests__/version-bump.test.ts similarity index 100% rename from lib/__tests__/version-bump.test.ts rename to apps/web/lib/__tests__/version-bump.test.ts diff --git a/lib/api/__tests__/auto-pin.test.ts b/apps/web/lib/api/__tests__/auto-pin.test.ts similarity index 100% rename from lib/api/__tests__/auto-pin.test.ts rename to apps/web/lib/api/__tests__/auto-pin.test.ts diff --git a/lib/api/__tests__/check-email.test.ts b/apps/web/lib/api/__tests__/check-email.test.ts similarity index 100% rename from lib/api/__tests__/check-email.test.ts rename to apps/web/lib/api/__tests__/check-email.test.ts diff --git a/lib/api/__tests__/domain-block-api.test.ts b/apps/web/lib/api/__tests__/domain-block-api.test.ts similarity index 100% rename from lib/api/__tests__/domain-block-api.test.ts rename to apps/web/lib/api/__tests__/domain-block-api.test.ts diff --git a/lib/api/__tests__/openapi-fixes.test.ts b/apps/web/lib/api/__tests__/openapi-fixes.test.ts similarity index 100% rename from lib/api/__tests__/openapi-fixes.test.ts rename to apps/web/lib/api/__tests__/openapi-fixes.test.ts diff --git a/lib/api/__tests__/rate-limit-removal.test.ts b/apps/web/lib/api/__tests__/rate-limit-removal.test.ts similarity index 100% rename from lib/api/__tests__/rate-limit-removal.test.ts rename to apps/web/lib/api/__tests__/rate-limit-removal.test.ts diff --git a/lib/api/admin-analytics.ts b/apps/web/lib/api/admin-analytics.ts similarity index 100% rename from lib/api/admin-analytics.ts rename to apps/web/lib/api/admin-analytics.ts diff --git a/lib/api/admin-comments.ts b/apps/web/lib/api/admin-comments.ts similarity index 100% rename from lib/api/admin-comments.ts rename to apps/web/lib/api/admin-comments.ts diff --git a/lib/api/admin-performance.ts b/apps/web/lib/api/admin-performance.ts similarity index 100% rename from lib/api/admin-performance.ts rename to apps/web/lib/api/admin-performance.ts diff --git a/lib/api/admin-reports.ts b/apps/web/lib/api/admin-reports.ts similarity index 100% rename from lib/api/admin-reports.ts rename to apps/web/lib/api/admin-reports.ts diff --git a/lib/api/admin-storage.ts b/apps/web/lib/api/admin-storage.ts similarity index 100% rename from lib/api/admin-storage.ts rename to apps/web/lib/api/admin-storage.ts diff --git a/lib/api/app.ts b/apps/web/lib/api/app.ts similarity index 100% rename from lib/api/app.ts rename to apps/web/lib/api/app.ts diff --git a/lib/api/auto-pin.ts b/apps/web/lib/api/auto-pin.ts similarity index 100% rename from lib/api/auto-pin.ts rename to apps/web/lib/api/auto-pin.ts diff --git a/lib/api/comments.ts b/apps/web/lib/api/comments.ts similarity index 100% rename from lib/api/comments.ts rename to apps/web/lib/api/comments.ts diff --git a/lib/api/community-suggestions.ts b/apps/web/lib/api/community-suggestions.ts similarity index 100% rename from lib/api/community-suggestions.ts rename to apps/web/lib/api/community-suggestions.ts diff --git a/lib/api/compare.ts b/apps/web/lib/api/compare.ts similarity index 100% rename from lib/api/compare.ts rename to apps/web/lib/api/compare.ts diff --git a/lib/api/contact.ts b/apps/web/lib/api/contact.ts similarity index 100% rename from lib/api/contact.ts rename to apps/web/lib/api/contact.ts diff --git a/lib/api/cron.ts b/apps/web/lib/api/cron.ts similarity index 100% rename from lib/api/cron.ts rename to apps/web/lib/api/cron.ts diff --git a/lib/api/crud-builder.ts b/apps/web/lib/api/crud-builder.ts similarity index 100% rename from lib/api/crud-builder.ts rename to apps/web/lib/api/crud-builder.ts diff --git a/lib/api/dashboard-public.ts b/apps/web/lib/api/dashboard-public.ts similarity index 100% rename from lib/api/dashboard-public.ts rename to apps/web/lib/api/dashboard-public.ts diff --git a/lib/api/dashboard.ts b/apps/web/lib/api/dashboard.ts similarity index 100% rename from lib/api/dashboard.ts rename to apps/web/lib/api/dashboard.ts diff --git a/lib/api/game-stats.ts b/apps/web/lib/api/game-stats.ts similarity index 100% rename from lib/api/game-stats.ts rename to apps/web/lib/api/game-stats.ts diff --git a/lib/api/game-stub.ts b/apps/web/lib/api/game-stub.ts similarity index 100% rename from lib/api/game-stub.ts rename to apps/web/lib/api/game-stub.ts diff --git a/lib/api/games-listing.ts b/apps/web/lib/api/games-listing.ts similarity index 100% rename from lib/api/games-listing.ts rename to apps/web/lib/api/games-listing.ts diff --git a/lib/api/games-manual.ts b/apps/web/lib/api/games-manual.ts similarity index 100% rename from lib/api/games-manual.ts rename to apps/web/lib/api/games-manual.ts diff --git a/lib/api/games-performance.ts b/apps/web/lib/api/games-performance.ts similarity index 100% rename from lib/api/games-performance.ts rename to apps/web/lib/api/games-performance.ts diff --git a/lib/api/games.ts b/apps/web/lib/api/games.ts similarity index 100% rename from lib/api/games.ts rename to apps/web/lib/api/games.ts diff --git a/lib/api/hardware-stats.ts b/apps/web/lib/api/hardware-stats.ts similarity index 100% rename from lib/api/hardware-stats.ts rename to apps/web/lib/api/hardware-stats.ts diff --git a/lib/api/hardware.ts b/apps/web/lib/api/hardware.ts similarity index 100% rename from lib/api/hardware.ts rename to apps/web/lib/api/hardware.ts diff --git a/lib/api/health.ts b/apps/web/lib/api/health.ts similarity index 100% rename from lib/api/health.ts rename to apps/web/lib/api/health.ts diff --git a/lib/api/index.ts b/apps/web/lib/api/index.ts similarity index 100% rename from lib/api/index.ts rename to apps/web/lib/api/index.ts diff --git a/lib/api/mobile.ts b/apps/web/lib/api/mobile.ts similarity index 100% rename from lib/api/mobile.ts rename to apps/web/lib/api/mobile.ts diff --git a/lib/api/performance-submit.ts b/apps/web/lib/api/performance-submit.ts similarity index 100% rename from lib/api/performance-submit.ts rename to apps/web/lib/api/performance-submit.ts diff --git a/lib/api/performance.ts b/apps/web/lib/api/performance.ts similarity index 100% rename from lib/api/performance.ts rename to apps/web/lib/api/performance.ts diff --git a/lib/api/playability.ts b/apps/web/lib/api/playability.ts similarity index 100% rename from lib/api/playability.ts rename to apps/web/lib/api/playability.ts diff --git a/lib/api/profile-photo.ts b/apps/web/lib/api/profile-photo.ts similarity index 100% rename from lib/api/profile-photo.ts rename to apps/web/lib/api/profile-photo.ts diff --git a/lib/api/reports.ts b/apps/web/lib/api/reports.ts similarity index 100% rename from lib/api/reports.ts rename to apps/web/lib/api/reports.ts diff --git a/lib/api/saved-filters.ts b/apps/web/lib/api/saved-filters.ts similarity index 100% rename from lib/api/saved-filters.ts rename to apps/web/lib/api/saved-filters.ts diff --git a/lib/api/saved-games.ts b/apps/web/lib/api/saved-games.ts similarity index 100% rename from lib/api/saved-games.ts rename to apps/web/lib/api/saved-games.ts diff --git a/lib/api/screenshots.ts b/apps/web/lib/api/screenshots.ts similarity index 100% rename from lib/api/screenshots.ts rename to apps/web/lib/api/screenshots.ts diff --git a/lib/api/search-unified.ts b/apps/web/lib/api/search-unified.ts similarity index 100% rename from lib/api/search-unified.ts rename to apps/web/lib/api/search-unified.ts diff --git a/lib/api/steam-reviews.ts b/apps/web/lib/api/steam-reviews.ts similarity index 100% rename from lib/api/steam-reviews.ts rename to apps/web/lib/api/steam-reviews.ts diff --git a/lib/api/steam-search.ts b/apps/web/lib/api/steam-search.ts similarity index 100% rename from lib/api/steam-search.ts rename to apps/web/lib/api/steam-search.ts diff --git a/lib/api/steamdb-version.ts b/apps/web/lib/api/steamdb-version.ts similarity index 100% rename from lib/api/steamdb-version.ts rename to apps/web/lib/api/steamdb-version.ts diff --git a/lib/api/steamgrid-proxy.ts b/apps/web/lib/api/steamgrid-proxy.ts similarity index 100% rename from lib/api/steamgrid-proxy.ts rename to apps/web/lib/api/steamgrid-proxy.ts diff --git a/lib/api/user.ts b/apps/web/lib/api/user.ts similarity index 100% rename from lib/api/user.ts rename to apps/web/lib/api/user.ts diff --git a/lib/api/version-test.ts b/apps/web/lib/api/version-test.ts similarity index 100% rename from lib/api/version-test.ts rename to apps/web/lib/api/version-test.ts diff --git a/lib/auth-client.ts b/apps/web/lib/auth-client.ts similarity index 100% rename from lib/auth-client.ts rename to apps/web/lib/auth-client.ts diff --git a/lib/auth.ts b/apps/web/lib/auth.ts similarity index 100% rename from lib/auth.ts rename to apps/web/lib/auth.ts diff --git a/lib/auth/__tests__/auth.test.ts b/apps/web/lib/auth/__tests__/auth.test.ts similarity index 100% rename from lib/auth/__tests__/auth.test.ts rename to apps/web/lib/auth/__tests__/auth.test.ts diff --git a/lib/auth/__tests__/domain-block.test.ts b/apps/web/lib/auth/__tests__/domain-block.test.ts similarity index 100% rename from lib/auth/__tests__/domain-block.test.ts rename to apps/web/lib/auth/__tests__/domain-block.test.ts diff --git a/lib/auth/__tests__/guard.test.ts b/apps/web/lib/auth/__tests__/guard.test.ts similarity index 100% rename from lib/auth/__tests__/guard.test.ts rename to apps/web/lib/auth/__tests__/guard.test.ts diff --git a/lib/auth/__tests__/password-strength.test.ts b/apps/web/lib/auth/__tests__/password-strength.test.ts similarity index 100% rename from lib/auth/__tests__/password-strength.test.ts rename to apps/web/lib/auth/__tests__/password-strength.test.ts diff --git a/lib/auth/__tests__/validation.test.ts b/apps/web/lib/auth/__tests__/validation.test.ts similarity index 100% rename from lib/auth/__tests__/validation.test.ts rename to apps/web/lib/auth/__tests__/validation.test.ts diff --git a/lib/auth/domain-block.ts b/apps/web/lib/auth/domain-block.ts similarity index 100% rename from lib/auth/domain-block.ts rename to apps/web/lib/auth/domain-block.ts diff --git a/lib/auth/email.ts b/apps/web/lib/auth/email.ts similarity index 100% rename from lib/auth/email.ts rename to apps/web/lib/auth/email.ts diff --git a/lib/auth/guard.ts b/apps/web/lib/auth/guard.ts similarity index 100% rename from lib/auth/guard.ts rename to apps/web/lib/auth/guard.ts diff --git a/lib/auth/password-strength.ts b/apps/web/lib/auth/password-strength.ts similarity index 100% rename from lib/auth/password-strength.ts rename to apps/web/lib/auth/password-strength.ts diff --git a/lib/auth/permissions.ts b/apps/web/lib/auth/permissions.ts similarity index 100% rename from lib/auth/permissions.ts rename to apps/web/lib/auth/permissions.ts diff --git a/lib/auth/test.ts b/apps/web/lib/auth/test.ts similarity index 100% rename from lib/auth/test.ts rename to apps/web/lib/auth/test.ts diff --git a/lib/auth/validation.ts b/apps/web/lib/auth/validation.ts similarity index 100% rename from lib/auth/validation.ts rename to apps/web/lib/auth/validation.ts diff --git a/lib/db/index.ts b/apps/web/lib/db/index.ts similarity index 100% rename from lib/db/index.ts rename to apps/web/lib/db/index.ts diff --git a/lib/db/migrations/backfill-slugs.ts b/apps/web/lib/db/migrations/backfill-slugs.ts similarity index 100% rename from lib/db/migrations/backfill-slugs.ts rename to apps/web/lib/db/migrations/backfill-slugs.ts diff --git a/lib/db/schema/auth.ts b/apps/web/lib/db/schema/auth.ts similarity index 100% rename from lib/db/schema/auth.ts rename to apps/web/lib/db/schema/auth.ts diff --git a/lib/db/schema/community-suggestions.ts b/apps/web/lib/db/schema/community-suggestions.ts similarity index 100% rename from lib/db/schema/community-suggestions.ts rename to apps/web/lib/db/schema/community-suggestions.ts diff --git a/lib/db/schema/entryScreenshots.ts b/apps/web/lib/db/schema/entryScreenshots.ts similarity index 100% rename from lib/db/schema/entryScreenshots.ts rename to apps/web/lib/db/schema/entryScreenshots.ts diff --git a/lib/db/schema/gameComments.ts b/apps/web/lib/db/schema/gameComments.ts similarity index 100% rename from lib/db/schema/gameComments.ts rename to apps/web/lib/db/schema/gameComments.ts diff --git a/lib/db/schema/gamePlatformSupport.ts b/apps/web/lib/db/schema/gamePlatformSupport.ts similarity index 100% rename from lib/db/schema/gamePlatformSupport.ts rename to apps/web/lib/db/schema/gamePlatformSupport.ts diff --git a/lib/db/schema/gameVersions.ts b/apps/web/lib/db/schema/gameVersions.ts similarity index 100% rename from lib/db/schema/gameVersions.ts rename to apps/web/lib/db/schema/gameVersions.ts diff --git a/lib/db/schema/games.ts b/apps/web/lib/db/schema/games.ts similarity index 100% rename from lib/db/schema/games.ts rename to apps/web/lib/db/schema/games.ts diff --git a/lib/db/schema/hardware.ts b/apps/web/lib/db/schema/hardware.ts similarity index 100% rename from lib/db/schema/hardware.ts rename to apps/web/lib/db/schema/hardware.ts diff --git a/lib/db/schema/index.ts b/apps/web/lib/db/schema/index.ts similarity index 100% rename from lib/db/schema/index.ts rename to apps/web/lib/db/schema/index.ts diff --git a/lib/db/schema/performanceEntries.ts b/apps/web/lib/db/schema/performanceEntries.ts similarity index 100% rename from lib/db/schema/performanceEntries.ts rename to apps/web/lib/db/schema/performanceEntries.ts diff --git a/lib/db/schema/reports.ts b/apps/web/lib/db/schema/reports.ts similarity index 100% rename from lib/db/schema/reports.ts rename to apps/web/lib/db/schema/reports.ts diff --git a/lib/db/schema/saved-filters.ts b/apps/web/lib/db/schema/saved-filters.ts similarity index 100% rename from lib/db/schema/saved-filters.ts rename to apps/web/lib/db/schema/saved-filters.ts diff --git a/lib/db/schema/savedGames.ts b/apps/web/lib/db/schema/savedGames.ts similarity index 100% rename from lib/db/schema/savedGames.ts rename to apps/web/lib/db/schema/savedGames.ts diff --git a/lib/db/schema/storage.ts b/apps/web/lib/db/schema/storage.ts similarity index 100% rename from lib/db/schema/storage.ts rename to apps/web/lib/db/schema/storage.ts diff --git a/lib/db/search.ts b/apps/web/lib/db/search.ts similarity index 100% rename from lib/db/search.ts rename to apps/web/lib/db/search.ts diff --git a/lib/db/seed.ts b/apps/web/lib/db/seed.ts similarity index 100% rename from lib/db/seed.ts rename to apps/web/lib/db/seed.ts diff --git a/lib/eden.ts b/apps/web/lib/eden.ts similarity index 100% rename from lib/eden.ts rename to apps/web/lib/eden.ts diff --git a/lib/hooks/use-gamepad-navigation.ts b/apps/web/lib/hooks/use-gamepad-navigation.ts similarity index 100% rename from lib/hooks/use-gamepad-navigation.ts rename to apps/web/lib/hooks/use-gamepad-navigation.ts diff --git a/lib/hooks/useDebounce.ts b/apps/web/lib/hooks/useDebounce.ts similarity index 100% rename from lib/hooks/useDebounce.ts rename to apps/web/lib/hooks/useDebounce.ts diff --git a/lib/image-processing.ts b/apps/web/lib/image-processing.ts similarity index 100% rename from lib/image-processing.ts rename to apps/web/lib/image-processing.ts diff --git a/lib/routes.ts b/apps/web/lib/routes.ts similarity index 100% rename from lib/routes.ts rename to apps/web/lib/routes.ts diff --git a/lib/sitemap-utils.ts b/apps/web/lib/sitemap-utils.ts similarity index 100% rename from lib/sitemap-utils.ts rename to apps/web/lib/sitemap-utils.ts diff --git a/lib/steam/__tests__/ensure-steam-game.test.ts b/apps/web/lib/steam/__tests__/ensure-steam-game.test.ts similarity index 100% rename from lib/steam/__tests__/ensure-steam-game.test.ts rename to apps/web/lib/steam/__tests__/ensure-steam-game.test.ts diff --git a/lib/steam/sync.ts b/apps/web/lib/steam/sync.ts similarity index 100% rename from lib/steam/sync.ts rename to apps/web/lib/steam/sync.ts diff --git a/lib/steamdb/__tests__/scrape.test.ts b/apps/web/lib/steamdb/__tests__/scrape.test.ts similarity index 100% rename from lib/steamdb/__tests__/scrape.test.ts rename to apps/web/lib/steamdb/__tests__/scrape.test.ts diff --git a/lib/steamdb/cache.ts b/apps/web/lib/steamdb/cache.ts similarity index 100% rename from lib/steamdb/cache.ts rename to apps/web/lib/steamdb/cache.ts diff --git a/lib/steamdb/scrape.ts b/apps/web/lib/steamdb/scrape.ts similarity index 100% rename from lib/steamdb/scrape.ts rename to apps/web/lib/steamdb/scrape.ts diff --git a/lib/storage/index.ts b/apps/web/lib/storage/index.ts similarity index 100% rename from lib/storage/index.ts rename to apps/web/lib/storage/index.ts diff --git a/lib/storage/r2-client.ts b/apps/web/lib/storage/r2-client.ts similarity index 100% rename from lib/storage/r2-client.ts rename to apps/web/lib/storage/r2-client.ts diff --git a/lib/updates.ts b/apps/web/lib/updates.ts similarity index 100% rename from lib/updates.ts rename to apps/web/lib/updates.ts diff --git a/lib/utils.ts b/apps/web/lib/utils.ts similarity index 100% rename from lib/utils.ts rename to apps/web/lib/utils.ts diff --git a/lib/utils/__tests__/seo.test.ts b/apps/web/lib/utils/__tests__/seo.test.ts similarity index 100% rename from lib/utils/__tests__/seo.test.ts rename to apps/web/lib/utils/__tests__/seo.test.ts diff --git a/lib/utils/__tests__/slug.test.ts b/apps/web/lib/utils/__tests__/slug.test.ts similarity index 100% rename from lib/utils/__tests__/slug.test.ts rename to apps/web/lib/utils/__tests__/slug.test.ts diff --git a/lib/utils/seo.ts b/apps/web/lib/utils/seo.ts similarity index 100% rename from lib/utils/seo.ts rename to apps/web/lib/utils/seo.ts diff --git a/lib/utils/slug.ts b/apps/web/lib/utils/slug.ts similarity index 100% rename from lib/utils/slug.ts rename to apps/web/lib/utils/slug.ts diff --git a/lib/version-fetchers/community-hub.ts b/apps/web/lib/version-fetchers/community-hub.ts similarity index 100% rename from lib/version-fetchers/community-hub.ts rename to apps/web/lib/version-fetchers/community-hub.ts diff --git a/lib/version-fetchers/index.ts b/apps/web/lib/version-fetchers/index.ts similarity index 100% rename from lib/version-fetchers/index.ts rename to apps/web/lib/version-fetchers/index.ts diff --git a/lib/version-fetchers/store-api.ts b/apps/web/lib/version-fetchers/store-api.ts similarity index 100% rename from lib/version-fetchers/store-api.ts rename to apps/web/lib/version-fetchers/store-api.ts diff --git a/lib/version-fetchers/store-page.ts b/apps/web/lib/version-fetchers/store-page.ts similarity index 100% rename from lib/version-fetchers/store-page.ts rename to apps/web/lib/version-fetchers/store-page.ts diff --git a/lib/version-fetchers/types.ts b/apps/web/lib/version-fetchers/types.ts similarity index 100% rename from lib/version-fetchers/types.ts rename to apps/web/lib/version-fetchers/types.ts diff --git a/lib/version-fetchers/uptodate-api.ts b/apps/web/lib/version-fetchers/uptodate-api.ts similarity index 100% rename from lib/version-fetchers/uptodate-api.ts rename to apps/web/lib/version-fetchers/uptodate-api.ts diff --git a/next.config.ts b/apps/web/next.config.ts similarity index 100% rename from next.config.ts rename to apps/web/next.config.ts diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..0aafd7c --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,88 @@ +{ + "name": "@deckyvault/web", + "version": "2026.2.2", + "private": true, + "scripts": { + "dev": "next dev --experimental-https --webpack", + "build": "next build --webpack", + "start": "next start", + "lint": "eslint", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "db:push": "drizzle-kit push", + "db:seed": "bun run lib/db/seed.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:ci": "vitest run" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1036.0", + "@better-auth/api-key": "^1.6.22", + "@better-auth/drizzle-adapter": "^1.6.9", + "@better-auth/expo": "^1.6.11", + "@better-auth/passkey": "^1.6.9", + "@elysia/cron": "^1.4.2", + "@elysia/eden": "^1.4.10", + "@elysia/openapi": "^1.4.15", + "@serwist/expiration": "^9.5.11", + "@serwist/next": "^9.5.11", + "@serwist/precaching": "^9.5.11", + "@serwist/routing": "^9.5.11", + "@serwist/strategies": "^9.5.11", + "@serwist/sw": "^9.5.11", + "@tiptap/core": "^3.22.4", + "@tiptap/extension-link": "^3.22.4", + "@tiptap/extension-placeholder": "^3.22.4", + "@tiptap/pm": "^3.22.4", + "@tiptap/react": "^3.22.4", + "@tiptap/starter-kit": "^3.22.4", + "better-auth": "^1.6.9", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "elysia": "^1.4.28", + "gray-matter": "^4.0.3", + "lucide-react": "^1.11.0", + "motion": "^12.38.0", + "next": "16.2.6", + "pg": "^8.20.0", + "react": "19.2.6", + "react-dom": "19.2.6", + "react-icons": "^5.6.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark": "^15.0.1", + "remark-rehype": "^11.1.2", + "resend": "^6.12.2", + "serwist": "^9.5.11", + "sharp": "^0.34.5", + "web-haptics": "^0.0.6" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@tailwindcss/typography": "^0.5.19", + "@types/node": "^20", + "@types/pg": "^8.20.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "@vitest/runner": "^4.1.5", + "drizzle-kit": "^0.31.10", + "eslint": "^9", + "eslint-config-next": "16.2.6", + "tailwindcss": "^4", + "tsx": "^4.21.0", + "typescript": "^5", + "vitest": "^4.1.5" + }, + "trustedDependencies": [ + "sharp", + "unrs-resolver" + ], + "overrides": { + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3" + } +} \ No newline at end of file diff --git a/postcss.config.mjs b/apps/web/postcss.config.mjs similarity index 100% rename from postcss.config.mjs rename to apps/web/postcss.config.mjs diff --git a/proxy.ts b/apps/web/proxy.ts similarity index 100% rename from proxy.ts rename to apps/web/proxy.ts diff --git a/public/.well-known/apple-app-site-association b/apps/web/public/.well-known/apple-app-site-association similarity index 100% rename from public/.well-known/apple-app-site-association rename to apps/web/public/.well-known/apple-app-site-association diff --git a/public/.well-known/assetlinks.json b/apps/web/public/.well-known/assetlinks.json similarity index 100% rename from public/.well-known/assetlinks.json rename to apps/web/public/.well-known/assetlinks.json diff --git a/public/icon-192.png b/apps/web/public/icon-192.png similarity index 100% rename from public/icon-192.png rename to apps/web/public/icon-192.png diff --git a/public/icon-512.png b/apps/web/public/icon-512.png similarity index 100% rename from public/icon-512.png rename to apps/web/public/icon-512.png diff --git a/public/icon.png b/apps/web/public/icon.png similarity index 100% rename from public/icon.png rename to apps/web/public/icon.png diff --git a/public/offline.html b/apps/web/public/offline.html similarity index 100% rename from public/offline.html rename to apps/web/public/offline.html diff --git a/tsconfig.json b/apps/web/tsconfig.json similarity index 100% rename from tsconfig.json rename to apps/web/tsconfig.json diff --git a/types/api.ts b/apps/web/types/api.ts similarity index 100% rename from types/api.ts rename to apps/web/types/api.ts diff --git a/vitest.config.ts b/apps/web/vitest.config.ts similarity index 100% rename from vitest.config.ts rename to apps/web/vitest.config.ts diff --git a/bun.lock b/bun.lock index dd13644..3a6682f 100644 --- a/bun.lock +++ b/bun.lock @@ -4,8 +4,13 @@ "workspaces": { "": { "name": "deckyvault", + }, + "apps/web": { + "name": "@deckyvault/web", + "version": "2026.2.2", "dependencies": { "@aws-sdk/client-s3": "^3.1036.0", + "@better-auth/api-key": "^1.6.22", "@better-auth/drizzle-adapter": "^1.6.9", "@better-auth/expo": "^1.6.11", "@better-auth/passkey": "^1.6.9", @@ -65,6 +70,26 @@ "vitest": "^4.1.5", }, }, + "packages/shared": { + "name": "@deckyvault/shared", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5", + }, + }, + "plugins/decky-vault": { + "name": "@deckyvault/plugin", + "version": "0.1.0", + "dependencies": { + "@deckyvault/shared": "workspace:*", + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^28", + "@rollup/plugin-typescript": "^12", + "rollup": "^4", + "typescript": "^5", + }, + }, }, "trustedDependencies": [ "sharp", @@ -189,6 +214,8 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@better-auth/api-key": ["@better-auth/api-key@1.6.22", "", { "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "better-auth": "^1.6.22", "better-call": "1.3.7" } }, "sha512-HDiiLYF0ov0zqhKv4CMTyLwpjTZ3UWl2dug451uTw40VM9zGxWSNxwILDcMDZ6hS5evaTHmmk7R5gciskEk2nQ=="], + "@better-auth/core": ["@better-auth/core@1.6.9", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w=="], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw=="], @@ -213,6 +240,12 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@deckyvault/plugin": ["@deckyvault/plugin@workspace:plugins/decky-vault"], + + "@deckyvault/shared": ["@deckyvault/shared@workspace:packages/shared"], + + "@deckyvault/web": ["@deckyvault/web@workspace:apps/web"], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@elysia/cron": ["@elysia/cron@1.4.2", "", { "dependencies": { "croner": "^6.0.3" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-YCaFCaT7CO7Lmfv66YGCqMYaAEjLZBpPtocaj9kS6NkhXqayhyKxqUOLSVW3qpIL9ZAE9m47GC650ZtvHpA58g=="], @@ -477,6 +510,62 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], + "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.9", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA=="], + + "@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.3.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@serwist/build": ["@serwist/build@9.5.11", "", { "dependencies": { "@serwist/utils": "9.5.11", "common-tags": "1.8.2", "glob": "13.0.6", "pretty-bytes": "6.1.1", "source-map": "0.8.0-beta.0", "type-fest": "5.6.0", "zod": "4.4.1" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-PQfW+LhADYFOOp0PhEnjlgJCyKor6cYa06d3rID1OpiKzkmCApJV1WYfdTBB96jXaWv6OWcWSbSV4tqDLxvaVA=="], @@ -713,7 +802,7 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], @@ -907,6 +996,8 @@ "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], @@ -1025,7 +1116,7 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1191,6 +1282,8 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], @@ -1529,7 +1622,7 @@ "resend": ["resend@6.12.2", "", { "dependencies": { "postal-mime": "2.7.4", "svix": "1.90.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw=="], - "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -1539,6 +1632,8 @@ "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1809,18 +1904,26 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "echarts/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], + "eslint/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-import-resolver-node/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "eslint-plugin-react-hooks/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -1897,6 +2000,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@vitest/mocker/estree-walker/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], diff --git a/package.json b/package.json index 8ded1e8..f16ea4c 100644 --- a/package.json +++ b/package.json @@ -1,91 +1,24 @@ { "name": "deckyvault", - "version": "2026.2.2", "private": true, + "workspaces": ["apps/*", "packages/*", "plugins/*"], "scripts": { - "dev": "next dev --experimental-https --webpack", - "build": "next build --webpack", - "start": "next start", - "lint": "eslint", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio", - "db:push": "drizzle-kit push", - "db:seed": "bun run lib/db/seed.ts", - "test": "vitest run", - "test:watch": "vitest", - "test:ci": "vitest run" + "dev": "bun run --filter @deckyvault/web dev", + "build": "bun run --filter @deckyvault/web build", + "start": "bun run --filter @deckyvault/web start", + "lint": "bun run --filter @deckyvault/web lint", + "test": "bun run --filter @deckyvault/web test", + "test:watch": "bun run --filter @deckyvault/web test:watch", + "test:ci": "bun run --filter @deckyvault/web test:ci", + "db:generate": "bun run --filter @deckyvault/web db:generate", + "db:migrate": "bun run --filter @deckyvault/web db:migrate", + "db:push": "bun run --filter @deckyvault/web db:push", + "db:studio": "bun run --filter @deckyvault/web db:studio", + "db:seed": "bun run --filter @deckyvault/web db:seed" }, - "dependencies": { - "@aws-sdk/client-s3": "^3.1036.0", - "@better-auth/drizzle-adapter": "^1.6.9", - "@better-auth/expo": "^1.6.11", - "@better-auth/passkey": "^1.6.9", - "@elysia/cron": "^1.4.2", - "@elysia/eden": "^1.4.10", - "@elysia/openapi": "^1.4.15", - "@serwist/expiration": "^9.5.11", - "@serwist/next": "^9.5.11", - "@serwist/precaching": "^9.5.11", - "@serwist/routing": "^9.5.11", - "@serwist/strategies": "^9.5.11", - "@serwist/sw": "^9.5.11", - "@tiptap/core": "^3.22.4", - "@tiptap/extension-link": "^3.22.4", - "@tiptap/extension-placeholder": "^3.22.4", - "@tiptap/pm": "^3.22.4", - "@tiptap/react": "^3.22.4", - "@tiptap/starter-kit": "^3.22.4", - "better-auth": "^1.6.9", - "dotenv": "^17.4.2", - "drizzle-orm": "^0.45.2", - "echarts": "^6.0.0", - "echarts-for-react": "^3.0.6", - "elysia": "^1.4.28", - "gray-matter": "^4.0.3", - "lucide-react": "^1.11.0", - "motion": "^12.38.0", - "next": "16.2.6", - "pg": "^8.20.0", - "react": "19.2.6", - "react-dom": "19.2.6", - "react-icons": "^5.6.0", - "rehype-autolink-headings": "^7.1.0", - "rehype-slug": "^6.0.0", - "rehype-stringify": "^10.0.1", - "remark": "^15.0.1", - "remark-rehype": "^11.1.2", - "resend": "^6.12.2", - "serwist": "^9.5.11", - "sharp": "^0.34.5", - "web-haptics": "^0.0.6" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@tailwindcss/typography": "^0.5.19", - "@types/node": "^20", - "@types/pg": "^8.20.0", - "@types/react": "19.2.14", - "@types/react-dom": "19.2.3", - "@vitest/runner": "^4.1.5", - "drizzle-kit": "^0.31.10", - "eslint": "^9", - "eslint-config-next": "16.2.6", - "tailwindcss": "^4", - "tsx": "^4.21.0", - "typescript": "^5", - "vitest": "^4.1.5" - }, - "ignoreScripts": [ - "sharp", - "unrs-resolver" - ], - "trustedDependencies": [ - "sharp", - "unrs-resolver" - ], + "trustedDependencies": ["sharp", "unrs-resolver"], "overrides": { "@types/react": "19.2.14", "@types/react-dom": "19.2.3" } -} +} \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..c3f891e --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,11 @@ +{ + "name": "@deckyvault/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "devDependencies": { + "typescript": "^5" + } +} \ No newline at end of file diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..d9c23f6 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,90 @@ +// ── DeckyVault Import Format (v1) ────────────────────────────── +// This is the format produced by the Decky Loader plugin +// and consumed by POST /api/performance/import + +export const VALID_UPSCALER_TYPES = [ + "none", "fsr", "dlss", "xess", "lsfg", "other", +] as const + +export const VALID_FRAME_GEN_METHODS = [ + "none", "fsr_fg", "dlss_fg", "lsfg", "other", +] as const + +export const VALID_ANTICHEAT_STATUSES = [ + "none", "supported", "unsupported", "unknown", +] as const + +export type UpscalerType = (typeof VALID_UPSCALER_TYPES)[number] +export type FrameGenMethod = (typeof VALID_FRAME_GEN_METHODS)[number] +export type AntiCheatStatus = (typeof VALID_ANTICHEAT_STATUSES)[number] + +export interface DeckyVaultImportV1 { + version: 1 + steamAppId: number + hardwareSlug: string + fpsAvg: number + fpsLow?: number | null + fpsOnePercentLow?: number | null + fpsHigh?: number | null + protonVersion?: string | null + osVersion?: string | null + upscalerType?: string + upscalerVersion?: string | null + frameGenMethod?: string + tdpWatts?: number | null + loadTimeSsd?: number | null + loadTimeSd?: number | null + launchOptions?: string | null + settingsJson?: unknown[] | null + userNotes?: string | null + customSystem?: boolean + antiCheatRelevant?: boolean + antiCheatName?: string | null + antiCheatStatus?: string +} + +// ── Known hardware slugs ─────────────────────────────────────── +export const KNOWN_HARDWARE_SLUGS = [ + "steamdeck-lcd", + "steamdeck-oled", + "steamdeck-lcd-64gb", + "steamdeck-lcd-256gb", + "steamdeck-lcd-512gb", + "steamdeck-oled-512gb", + "steamdeck-oled-1tb", + "rog-ally", + "rog-ally-x", + "legion-go", + "msi-claw", +] as const + +export type HardwareSlug = (typeof KNOWN_HARDWARE_SLUGS)[number] + +// ── API Response Types ────────────────────────────────────────── +export interface GameLookupResponse { + game: { + id: string + steamAppId: number | null + title: string + slug: string | null + headerImage: string | null + capsuleImage: string | null + developer: string | null + publisher: string | null + } + version: { + id: string + versionString: string | null + buildId: string | null + isLatest: boolean + createdAt: string + } | null +} + +export interface ImportResponse { + id: string + gameId: string + versionId: string + createdAt: string + authMethod: "session" | "api-key" +} \ No newline at end of file diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..0751e2d --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} \ No newline at end of file diff --git a/plugins/decky-vault/package.json b/plugins/decky-vault/package.json new file mode 100644 index 0000000..a45e0b6 --- /dev/null +++ b/plugins/decky-vault/package.json @@ -0,0 +1,19 @@ +{ + "name": "@deckyvault/plugin", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w" + }, + "dependencies": { + "@deckyvault/shared": "workspace:*" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^28", + "@rollup/plugin-typescript": "^12", + "rollup": "^4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/plugins/decky-vault/plugin.json b/plugins/decky-vault/plugin.json new file mode 100644 index 0000000..327cfaa --- /dev/null +++ b/plugins/decky-vault/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "DeckyVault", + "version": "0.1.0", + "description": "Automatically record performance metrics and export/upload to DeckyVault", + "author": "DeckyVault", + "license": "MIT", + "icon": "", + "permissions": ["system"] +} \ No newline at end of file diff --git a/plugins/decky-vault/rollup.config.js b/plugins/decky-vault/rollup.config.js new file mode 100644 index 0000000..283bdcc --- /dev/null +++ b/plugins/decky-vault/rollup.config.js @@ -0,0 +1,20 @@ +import typescript from "@rollup/plugin-typescript" +import commonjs from "@rollup/plugin-commonjs" + +export default { + input: "src/index.tsx", + output: { + file: "dist/index.js", + format: "esm", + sourcemap: false, + }, + plugins: [ + typescript(), + commonjs(), + ], + external: [ + "react", + "react-dom", + "@deckyvault/shared", + ], +} \ No newline at end of file diff --git a/plugins/decky-vault/src/index.tsx b/plugins/decky-vault/src/index.tsx new file mode 100644 index 0000000..178425e --- /dev/null +++ b/plugins/decky-vault/src/index.tsx @@ -0,0 +1,32 @@ +import { DeckyVaultImportV1 } from "@deckyvault/shared" + +interface PluginSettings { + apiKey: string + autoRecord: boolean + exportPath: string +} + +let settings: PluginSettings = { + apiKey: "", + autoRecord: false, + exportPath: "/home/deck/Downloads", +} + +export default { + name: "DeckyVault", + content: () => { + // Main plugin UI — will be implemented in a future phase + return
DeckyVault Plugin
+ }, + onSettingUpdate: (newSettings: Partial) => { + settings = { ...settings, ...newSettings } + }, + onGameSessionStart: (appId: number) => { + DeckyPlugin.log(`[DeckyVault] Game started: ${appId}`) + // Future: start MangoHud monitoring + }, + onGameSessionEnd: (appId: number) => { + DeckyPlugin.log(`[DeckyVault] Game stopped: ${appId}`) + // Future: stop monitoring, prompt export/upload + }, +} \ No newline at end of file diff --git a/plugins/decky-vault/src/types.d.ts b/plugins/decky-vault/src/types.d.ts new file mode 100644 index 0000000..b7fc8fb --- /dev/null +++ b/plugins/decky-vault/src/types.d.ts @@ -0,0 +1,32 @@ +// Decky Loader API type declarations +// These mirror the APIs available in the Steam Deck game mode CEF context + +declare global { + const DeckyPlugin: { + log: (...args: unknown[]) => void + debug: (...args: unknown[]) => void + info: (...args: unknown[]) => void + error: (...args: unknown[]) => void + } + + const SteamClient: { + Apps: { + GetAppData: (appId: number) => Promise<{ + strAppName: string + strShortcutName: string + strExePath: string + }> + RegisterForGameStarted: ( + callback: (appId: number) => void, + ) => { unregister: () => void } + RegisterForGameStopped: ( + callback: (appId: number) => void, + ) => { unregister: () => void } + } + System: { + GetOSVersion: () => Promise + } + } +} + +export {} \ No newline at end of file diff --git a/plugins/decky-vault/tsconfig.json b/plugins/decky-vault/tsconfig.json new file mode 100644 index 0000000..cc6d5e7 --- /dev/null +++ b/plugins/decky-vault/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "outDir": "./dist", + "rootDir": "./src", + "declaration": false, + "sourceMap": false, + "esModuleInterop": true + }, + "include": ["src"], + "references": [ + { "path": "../../packages/shared" } + ] +} \ No newline at end of file From f7eda2bfc009775d3e392c226c3be5fb6dba9fa0 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 28 Jun 2026 05:30:48 +0800 Subject: [PATCH 2/2] feat: add better-auth api-key plugin, import endpoint, and API key UI --- bun.lock | 3 + components/profile/settings-api-keys-tab.tsx | 474 ++++ components/profile/settings-container.tsx | 9 +- drizzle/0027_slimy_stryfe.sql | 28 + drizzle/meta/0027_snapshot.json | 2640 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/api/app.ts | 4 + lib/api/games-lookup.ts | 93 + lib/api/index.ts | 2 + lib/api/performance-import.ts | 284 ++ lib/auth-client.ts | 4 +- lib/auth.ts | 14 + lib/auth/api-key-guard.ts | 98 + lib/db/schema/auth.ts | 43 + package.json | 1 + 15 files changed, 3701 insertions(+), 3 deletions(-) create mode 100644 components/profile/settings-api-keys-tab.tsx create mode 100644 drizzle/0027_slimy_stryfe.sql create mode 100644 drizzle/meta/0027_snapshot.json create mode 100644 lib/api/games-lookup.ts create mode 100644 lib/api/performance-import.ts create mode 100644 lib/auth/api-key-guard.ts diff --git a/bun.lock b/bun.lock index dd13644..ff0f349 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "deckyvault", "dependencies": { "@aws-sdk/client-s3": "^3.1036.0", + "@better-auth/api-key": "^1.6.22", "@better-auth/drizzle-adapter": "^1.6.9", "@better-auth/expo": "^1.6.11", "@better-auth/passkey": "^1.6.9", @@ -189,6 +190,8 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@better-auth/api-key": ["@better-auth/api-key@1.6.22", "", { "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.22", "@better-auth/utils": "0.4.2", "better-auth": "^1.6.22", "better-call": "1.3.7" } }, "sha512-HDiiLYF0ov0zqhKv4CMTyLwpjTZ3UWl2dug451uTw40VM9zGxWSNxwILDcMDZ6hS5evaTHmmk7R5gciskEk2nQ=="], + "@better-auth/core": ["@better-auth/core@1.6.9", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-ADFk5pwmLybmc+LvYvXJ6M1x2oY/EyYLkwLuH0x28FUq12DfjL0wnE7g+WRDf3yozDO+qIxTpFGXDGwLKbfz0w=="], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw=="], diff --git a/components/profile/settings-api-keys-tab.tsx b/components/profile/settings-api-keys-tab.tsx new file mode 100644 index 0000000..12df467 --- /dev/null +++ b/components/profile/settings-api-keys-tab.tsx @@ -0,0 +1,474 @@ +"use client" + +import { useState, useEffect, useCallback } from "react" +import { authClient } from "@/lib/auth-client" +import { + Loader2, + Key, + Plus, + Trash2, + Copy, + Check, + Eye, + EyeOff, + Clock, + AlertCircle, +} from "lucide-react" +import { motion, AnimatePresence } from "motion/react" + +// Raw API key type from Better Auth (dates are Date objects from the API) +interface RawApiKey { + id: string + name: string | null + start: string | null + prefix: string | null + enabled: boolean + expiresAt: Date | null + lastRequest: Date | null + remaining: number | null + createdAt: Date + updatedAt: Date + referenceId: string + metadata: Record | null + permissions: Record | null + configId: string + refillInterval: number | null + refillAmount: number | null + lastRefillAt: Date | null + rateLimitEnabled: boolean + rateLimitTimeWindow: number | null + rateLimitMax: number | null + requestCount: number +} + +interface CreatedApiKey extends RawApiKey { + key: string // Only returned on creation +} + +export function SettingsApiKeysTab() { + const [apiKeys, setApiKeys] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + // Create form state + const [showCreateForm, setShowCreateForm] = useState(false) + const [newKeyName, setNewKeyName] = useState("") + const [newKeyExpiresIn, setNewKeyExpiresIn] = useState("") + const [isCreating, setIsCreating] = useState(false) + const [createdKey, setCreatedKey] = useState(null) + const [createError, setCreateError] = useState(null) + + // Delete state + const [deletingId, setDeletingId] = useState(null) + const [message, setMessage] = useState<{ + type: "success" | "error" + text: string + } | null>(null) + + // Copy state + const [copied, setCopied] = useState(false) + const [showKey, setShowKey] = useState(false) + + const fetchApiKeys = useCallback(async () => { + try { + const { data, error } = await authClient.apiKey.list({}) + if (error) { + setError(error.message || "Failed to load API keys") + setApiKeys([]) + } else { + setApiKeys((data?.apiKeys ?? []) as RawApiKey[]) + setError(null) + } + } catch { + setError("Failed to load API keys. Please try again.") + setApiKeys([]) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + fetchApiKeys() + }, [fetchApiKeys]) + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault() + setCreateError(null) + + if (!newKeyName.trim()) { + setCreateError("Name is required") + return + } + + setIsCreating(true) + try { + const expiresIn = newKeyExpiresIn + ? parseInt(newKeyExpiresIn) * 24 * 60 * 60 + : undefined + + const { data, error } = await authClient.apiKey.create({ + name: newKeyName.trim(), + expiresIn, + }) + + if (error) { + setCreateError(error.message || "Failed to create API key") + } else if (data) { + setCreatedKey(data as unknown as CreatedApiKey) + await fetchApiKeys() + setNewKeyName("") + setNewKeyExpiresIn("") + setShowCreateForm(false) + } + } catch { + setCreateError("An unexpected error occurred") + } finally { + setIsCreating(false) + } + } + + const handleDelete = async (keyId: string) => { + setDeletingId(keyId) + setMessage(null) + try { + const { error } = await authClient.apiKey.delete({ keyId }) + if (error) { + setMessage({ + type: "error", + text: error.message || "Failed to delete API key", + }) + } else { + setMessage({ type: "success", text: "API key deleted" }) + await fetchApiKeys() + } + } catch { + setMessage({ + type: "error", + text: "Failed to delete API key. Please try again.", + }) + } finally { + setDeletingId(null) + } + } + + const formatDate = (date: Date | string | null): string => { + if (!date) return "Never" + try { + const d = date instanceof Date ? date : new Date(date) + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }) + } catch { + return String(date) + } + } + + const handleCopyKey = async () => { + if (createdKey?.key) { + try { + await navigator.clipboard.writeText(createdKey.key) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + // Fallback for older browsers + const textarea = document.createElement("textarea") + textarea.value = createdKey.key + document.body.appendChild(textarea) + textarea.select() + document.execCommand("copy") + document.body.removeChild(textarea) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + } + + + + // Show the created key modal + if (createdKey) { + return ( + +
+
+ +

+ API Key Created +

+
+ +

+ Copy your API key now. You won't be able to see it again. +

+ +
+
+ {showKey + ? createdKey.key + : `${createdKey.key.substring(0, 12)}${"•".repeat(Math.min(createdKey.key.length - 12, 20))}`} +
+
+ + +
+
+ + {createdKey.name && ( +

+ Name: {createdKey.name} +

+ )} + {createdKey.expiresAt && ( +

+ Expires:{" "} + + {formatDate(createdKey.expiresAt)} + +

+ )} + + +
+
+ ) + } + + return ( + + {/* Header */} +
+

+ + API Keys +

+ +
+ + {/* Message */} + {message && ( +
+ {message.text} +
+ )} + + {/* Create Form */} + + {showCreateForm && ( + +
+
+ + setNewKeyName(e.target.value)} + className='w-full px-3 py-2 rounded-lg bg-text/5 border border-border text-sm text-text focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/30 transition-colors' + placeholder='e.g. Decky Loader Plugin' + maxLength={32} + required + /> +
+
+ + setNewKeyExpiresIn(e.target.value)} + className='w-full px-3 py-2 rounded-lg bg-text/5 border border-border text-sm text-text focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/30 transition-colors' + placeholder='Leave empty for no expiry' + min={1} + max={365} + /> +
+ + {createError && ( +

+ + {createError} +

+ )} + + +
+
+ )} +
+ + {/* Key List */} + {error ? ( +
+

{error}

+ +
+ ) : isLoading ? ( +
+ + Loading API keys... +
+ ) : apiKeys.length === 0 ? ( +
+ +

+ No API keys created yet. +

+

+ Create an API key to use with the Decky Loader plugin or other external tools. +

+
+ ) : ( +
+ + {apiKeys.map((ak) => ( + +
+
+ +

+ {ak.name || "Unnamed key"} +

+ {!ak.enabled && ( + + Disabled + + )} +
+
+ {ak.start && ( + + {ak.start}... + + )} + {ak.expiresAt && ( + + + Expires {formatDate(ak.expiresAt)} + + )} + {ak.lastRequest && ( + + Last used {formatDate(ak.lastRequest)} + + )} + + Created {formatDate(ak.createdAt)} + +
+
+ +
+ ))} +
+
+ )} + + {/* Info */} +
+

+ Using API keys: Pass your API key as the{" "} + x-api-key header when + making requests to DeckyVault's API. You can use these keys with the Decky + Loader plugin or any automation tool. +

+
+
+ ) +} \ No newline at end of file diff --git a/components/profile/settings-container.tsx b/components/profile/settings-container.tsx index 7a0a0cf..f9a0820 100644 --- a/components/profile/settings-container.tsx +++ b/components/profile/settings-container.tsx @@ -1,10 +1,11 @@ "use client" import { useState, useEffect } from "react" -import { User, Shield, Link as LinkIcon } from "lucide-react" +import { User, Shield, Link as LinkIcon, Key } from "lucide-react" import { SettingsProfileTab } from "@/components/profile/settings-profile-tab" import { SettingsSecurityTab } from "@/components/profile/settings-security-tab" import { SettingsAccountsTab } from "@/components/profile/settings-accounts-tab" +import { SettingsApiKeysTab } from "@/components/profile/settings-api-keys-tab" interface AuthMethods { hasPassword: boolean @@ -13,12 +14,13 @@ interface AuthMethods { totalAuthMethods: number } -type SettingsSubTab = "profile" | "security" | "accounts" +type SettingsSubTab = "profile" | "security" | "accounts" | "api-keys" const subTabs: { id: SettingsSubTab; label: string; icon: typeof User }[] = [ { id: "profile", label: "Profile", icon: User }, { id: "security", label: "Security", icon: Shield }, { id: "accounts", label: "Linked Accounts", icon: LinkIcon }, + { id: "api-keys", label: "API Keys", icon: Key }, ] interface SettingsContainerProps { @@ -145,6 +147,9 @@ export function SettingsContainer({ onRefreshAuthMethods={refreshAuthMethods} /> )} + {activeSubTab === "api-keys" && ( + + )} ) diff --git a/drizzle/0027_slimy_stryfe.sql b/drizzle/0027_slimy_stryfe.sql new file mode 100644 index 0000000..174a74e --- /dev/null +++ b/drizzle/0027_slimy_stryfe.sql @@ -0,0 +1,28 @@ +CREATE TABLE "apikey" ( + "id" text PRIMARY KEY NOT NULL, + "config_id" text DEFAULT 'default' NOT NULL, + "name" text, + "start" text, + "reference_id" text NOT NULL, + "prefix" text, + "key" text NOT NULL, + "refill_interval" integer, + "refill_amount" integer, + "last_refill_at" timestamp, + "enabled" boolean DEFAULT true NOT NULL, + "rate_limit_enabled" boolean DEFAULT true NOT NULL, + "rate_limit_time_window" integer, + "rate_limit_max" integer, + "request_count" integer DEFAULT 0 NOT NULL, + "remaining" integer, + "last_request" timestamp, + "expires_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "permissions" text, + "metadata" text +); +--> statement-breakpoint +CREATE INDEX "apikey_config_id_idx" ON "apikey" USING btree ("config_id");--> statement-breakpoint +CREATE INDEX "apikey_reference_id_idx" ON "apikey" USING btree ("reference_id");--> statement-breakpoint +CREATE INDEX "apikey_key_idx" ON "apikey" USING btree ("key"); \ No newline at end of file diff --git a/drizzle/meta/0027_snapshot.json b/drizzle/meta/0027_snapshot.json new file mode 100644 index 0000000..0954fb3 --- /dev/null +++ b/drizzle/meta/0027_snapshot.json @@ -0,0 +1,2640 @@ +{ + "id": "1e0b0a96-5871-49cf-9081-29944205d936", + "prevId": "7e7813e4-686e-4d98-b36f-fa2a2a82fa5d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_login_method": { + "name": "last_login_method", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_suggestions": { + "name": "community_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_name": { + "name": "field_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_value": { + "name": "current_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_value": { + "name": "proposed_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "suggestion_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "review_note": { + "name": "review_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "community_suggestions_game_field_user": { + "name": "community_suggestions_game_field_user", + "columns": [ + { + "expression": "game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "field_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "suggestions_status_idx": { + "name": "suggestions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "community_suggestions_game_id_games_id_fk": { + "name": "community_suggestions_game_id_games_id_fk", + "tableFrom": "community_suggestions", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "community_suggestions_user_id_user_id_fk": { + "name": "community_suggestions_user_id_user_id_fk", + "tableFrom": "community_suggestions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "community_suggestions_reviewed_by_user_id_fk": { + "name": "community_suggestions_reviewed_by_user_id_fk", + "tableFrom": "community_suggestions", + "tableTo": "user", + "columnsFrom": [ + "reviewed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entry_screenshots": { + "name": "entry_screenshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_index": { + "name": "order_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entry_screenshots_entry_idx": { + "name": "entry_screenshots_entry_idx", + "columns": [ + { + "expression": "entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entry_screenshots_entry_id_performance_entries_id_fk": { + "name": "entry_screenshots_entry_id_performance_entries_id_fk", + "tableFrom": "entry_screenshots", + "tableTo": "performance_entries", + "columnsFrom": [ + "entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_comments": { + "name": "game_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_removed": { + "name": "is_removed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comments_game_created_idx": { + "name": "comments_game_created_idx", + "columns": [ + { + "expression": "game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comments_parent_idx": { + "name": "comments_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_comments_game_id_games_id_fk": { + "name": "game_comments_game_id_games_id_fk", + "tableFrom": "game_comments", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_comments_user_id_user_id_fk": { + "name": "game_comments_user_id_user_id_fk", + "tableFrom": "game_comments", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_comments_parent_id_game_comments_id_fk": { + "name": "game_comments_parent_id_game_comments_id_fk", + "tableFrom": "game_comments", + "tableTo": "game_comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_platform_support": { + "name": "game_platform_support", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hardware_slug": { + "name": "hardware_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_supported": { + "name": "is_supported", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "proton_status": { + "name": "proton_status", + "type": "proton_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "anti_cheat_relevant": { + "name": "anti_cheat_relevant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "anti_cheat_name": { + "name": "anti_cheat_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anti_cheat_version": { + "name": "anti_cheat_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anti_cheat_status": { + "name": "anti_cheat_status", + "type": "anti_cheat_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "playability_status": { + "name": "playability_status", + "type": "playability_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "playability_override": { + "name": "playability_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "playability_calculated_at": { + "name": "playability_calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "game_platform_support_game_id_games_id_fk": { + "name": "game_platform_support_game_id_games_id_fk", + "tableFrom": "game_platform_support", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_platform_support_hardware_slug_hardware_slug_fk": { + "name": "game_platform_support_hardware_slug_hardware_slug_fk", + "tableFrom": "game_platform_support", + "tableTo": "hardware", + "columnsFrom": [ + "hardware_slug" + ], + "columnsTo": [ + "slug" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "game_hardware_unique": { + "name": "game_hardware_unique", + "nullsNotDistinct": false, + "columns": [ + "game_id", + "hardware_slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_versions": { + "name": "game_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version_string": { + "name": "version_string", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "perf_game_lookup_idx": { + "name": "perf_game_lookup_idx", + "columns": [ + { + "expression": "game_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_versions_game_id_games_id_fk": { + "name": "game_versions_game_id_games_id_fk", + "tableFrom": "game_versions", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "game_build_unique": { + "name": "game_build_unique", + "nullsNotDistinct": false, + "columns": [ + "game_id", + "build_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.games": { + "name": "games", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "steam_app_id": { + "name": "steam_app_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "game_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'steam'" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "developer": { + "name": "developer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genres": { + "name": "genres", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_image": { + "name": "header_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capsule_image": { + "name": "capsule_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "store_url": { + "name": "store_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "online_multiplayer_status": { + "name": "online_multiplayer_status", + "type": "online_multiplayer_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "system_requirements": { + "name": "system_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metacritic_score": { + "name": "metacritic_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metacritic_url": { + "name": "metacritic_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recommendations_total": { + "name": "recommendations_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steam_review_score": { + "name": "steam_review_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steam_review_sentiment": { + "name": "steam_review_sentiment", + "type": "steam_review_sentiment", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "steam_review_count": { + "name": "steam_review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "playability_status": { + "name": "playability_status", + "type": "playability_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "playability_override": { + "name": "playability_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "playability_calculated_at": { + "name": "playability_calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "price_current": { + "name": "price_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "price_initial": { + "name": "price_initial", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "price_currency": { + "name": "price_currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "release_date": { + "name": "release_date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "platforms": { + "name": "platforms", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_retry_count": { + "name": "sync_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_next_retry": { + "name": "sync_next_retry", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "games_source_idx": { + "name": "games_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "games_sync_status_idx": { + "name": "games_sync_status_idx", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "games_steam_app_id_unique": { + "name": "games_steam_app_id_unique", + "nullsNotDistinct": false, + "columns": [ + "steam_app_id" + ] + }, + "games_slug_unique": { + "name": "games_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hardware": { + "name": "hardware", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "device_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watt_hours": { + "name": "watt_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "tdp_max": { + "name": "tdp_max", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.performance_entries": { + "name": "performance_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hardware_slug": { + "name": "hardware_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fps_avg": { + "name": "fps_avg", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "fps_low": { + "name": "fps_low", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "fps_one_percent_low": { + "name": "fps_one_percent_low", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "fps_high": { + "name": "fps_high", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "proton_version": { + "name": "proton_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "os_version": { + "name": "os_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upscaler_type": { + "name": "upscaler_type", + "type": "upscaler_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "upscaler_version": { + "name": "upscaler_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frame_gen_method": { + "name": "frame_gen_method", + "type": "frame_gen_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "load_time_ssd": { + "name": "load_time_ssd", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "load_time_sd": { + "name": "load_time_sd", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "launch_options": { + "name": "launch_options", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_notes": { + "name": "user_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tdp_watts": { + "name": "tdp_watts", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "youtube_video_id": { + "name": "youtube_video_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_system": { + "name": "custom_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_removed": { + "name": "is_removed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "removed_reason": { + "name": "removed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "downvotes": { + "name": "downvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "verified_by": { + "name": "verified_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "perf_hardware_upscaler_idx": { + "name": "perf_hardware_upscaler_idx", + "columns": [ + { + "expression": "hardware_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "upscaler_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "perf_version_idx": { + "name": "perf_version_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "perf_user_idx": { + "name": "perf_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "perf_removed_created_idx": { + "name": "perf_removed_created_idx", + "columns": [ + { + "expression": "is_removed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "perf_upvotes_idx": { + "name": "perf_upvotes_idx", + "columns": [ + { + "expression": "upvotes", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "performance_entries_version_id_game_versions_id_fk": { + "name": "performance_entries_version_id_game_versions_id_fk", + "tableFrom": "performance_entries", + "tableTo": "game_versions", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "performance_entries_hardware_slug_hardware_slug_fk": { + "name": "performance_entries_hardware_slug_hardware_slug_fk", + "tableFrom": "performance_entries", + "tableTo": "hardware", + "columnsFrom": [ + "hardware_slug" + ], + "columnsTo": [ + "slug" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "performance_entries_user_id_user_id_fk": { + "name": "performance_entries_user_id_user_id_fk", + "tableFrom": "performance_entries", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "performance_entries_verified_by_user_id_fk": { + "name": "performance_entries_verified_by_user_id_fk", + "tableFrom": "performance_entries", + "tableTo": "user", + "columnsFrom": [ + "verified_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_games": { + "name": "saved_games", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "game_id": { + "name": "game_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "saved_games_user_id_user_id_fk": { + "name": "saved_games_user_id_user_id_fk", + "tableFrom": "saved_games", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_games_game_id_games_id_fk": { + "name": "saved_games_game_id_games_id_fk", + "tableFrom": "saved_games", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "saved_games_user_game_unique": { + "name": "saved_games_user_game_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "game_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "report_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "report_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_entry_reporter_unique": { + "name": "reports_entry_reporter_unique", + "columns": [ + { + "expression": "entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reports_entry_id_performance_entries_id_fk": { + "name": "reports_entry_id_performance_entries_id_fk", + "tableFrom": "reports", + "tableTo": "performance_entries", + "columnsFrom": [ + "entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reports_reporter_id_user_id_fk": { + "name": "reports_reporter_id_user_id_fk", + "tableFrom": "reports", + "tableTo": "user", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_filters": { + "name": "saved_filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filters": { + "name": "filters", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "saved_filters_user_id_user_id_fk": { + "name": "saved_filters_user_id_user_id_fk", + "tableFrom": "saved_filters", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "saved_filters_user_name": { + "name": "saved_filters_user_name", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_objects": { + "name": "storage_objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_orphaned": { + "name": "is_orphaned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "storage_entity_idx": { + "name": "storage_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "storage_key_idx": { + "name": "storage_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "storage_objects_uploaded_by_user_id_fk": { + "name": "storage_objects_uploaded_by_user_id_fk", + "tableFrom": "storage_objects", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.suggestion_status": { + "name": "suggestion_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.anti_cheat_status": { + "name": "anti_cheat_status", + "schema": "public", + "values": [ + "none", + "supported", + "unsupported", + "unknown" + ] + }, + "public.proton_status": { + "name": "proton_status", + "schema": "public", + "values": [ + "native", + "proton", + "unsupported", + "unknown" + ] + }, + "public.game_source": { + "name": "game_source", + "schema": "public", + "values": [ + "steam", + "manual", + "gog", + "epic" + ] + }, + "public.online_multiplayer_status": { + "name": "online_multiplayer_status", + "schema": "public", + "values": [ + "none", + "supported", + "unknown" + ] + }, + "public.playability_status": { + "name": "playability_status", + "schema": "public", + "values": [ + "great", + "playable", + "needs_tweaks", + "unplayable", + "unknown" + ] + }, + "public.steam_review_sentiment": { + "name": "steam_review_sentiment", + "schema": "public", + "values": [ + "overwhelmingly_positive", + "very_positive", + "positive", + "mostly_positive", + "mixed", + "mostly_negative", + "negative", + "very_negative", + "overwhelmingly_negative" + ] + }, + "public.device_type": { + "name": "device_type", + "schema": "public", + "values": [ + "handheld", + "console" + ] + }, + "public.frame_gen_method": { + "name": "frame_gen_method", + "schema": "public", + "values": [ + "none", + "fsr_fg", + "dlss_fg", + "lsfg", + "other" + ] + }, + "public.upscaler_type": { + "name": "upscaler_type", + "schema": "public", + "values": [ + "none", + "fsr", + "dlss", + "xess", + "lsfg", + "other" + ] + }, + "public.report_reason": { + "name": "report_reason", + "schema": "public", + "values": [ + "inaccurate", + "spam", + "inappropriate", + "other" + ] + }, + "public.report_status": { + "name": "report_status", + "schema": "public", + "values": [ + "open", + "reviewed", + "dismissed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 86a87e5..4d2d5b4 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -190,6 +190,13 @@ "when": 1779214557250, "tag": "0026_polite_drax", "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1782593630361, + "tag": "0027_slimy_stryfe", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/api/app.ts b/lib/api/app.ts index ccc9938..508f2cb 100644 --- a/lib/api/app.ts +++ b/lib/api/app.ts @@ -50,6 +50,8 @@ import { db } from "@/lib/db" import { user } from "@/lib/db/schema/auth" import { eq } from "drizzle-orm" import { mobileRoutes } from "@/lib/api/mobile" +import { gamesLookupRoutes } from "@/lib/api/games-lookup" +import { performanceImportRoutes } from "@/lib/api/performance-import" const betterAuth = new Elysia({ name: "better-auth" }) .mount(auth.handler) @@ -241,6 +243,7 @@ export const app = new Elysia({ prefix: "/api" }) .use(standaloneVersionTestRoutes) .use(gamesManualRoutes) .use(screenshotRoutes) + .use(gamesLookupRoutes) .use(mobileRoutes) ) // ── Write routes ─────────────────────────────────────────── @@ -256,6 +259,7 @@ export const app = new Elysia({ prefix: "/api" }) .use(adminPerformanceRoutes) .use(adminCommentRoutes) .use(adminStorageRoutes) + .use(performanceImportRoutes) .use(adminAnalyticsRoutes) ) // ── Public forms (no auth) ───────────────────────────────── diff --git a/lib/api/games-lookup.ts b/lib/api/games-lookup.ts new file mode 100644 index 0000000..b4225e9 --- /dev/null +++ b/lib/api/games-lookup.ts @@ -0,0 +1,93 @@ +import { Elysia, t } from "elysia" +import { db } from "@/lib/db/index" +import { games, gameVersions } from "@/lib/db/schema" +import { eq, and } from "drizzle-orm" + +export const gamesLookupRoutes = new Elysia({ + prefix: "/games", + detail: { tags: ["Games"] }, +}).get( + "/lookup", + async ({ query, set }) => { + const { steamAppId } = query + + if (!steamAppId) { + set.status = 400 + return { error: "steamAppId query parameter is required" } + } + + // Look up the game + const [game] = await db + .select({ + id: games.id, + steamAppId: games.steamAppId, + title: games.title, + slug: games.slug, + headerImage: games.headerImage, + capsuleImage: games.capsuleImage, + developer: games.developer, + publisher: games.publisher, + source: games.source, + }) + .from(games) + .where(eq(games.steamAppId, steamAppId)) + .limit(1) + + if (!game) { + set.status = 404 + return { error: `No game found with steamAppId ${steamAppId}` } + } + + // Find the latest version + const [latestVersion] = await db + .select({ + id: gameVersions.id, + versionString: gameVersions.versionString, + buildId: gameVersions.buildId, + isLatest: gameVersions.isLatest, + createdAt: gameVersions.createdAt, + }) + .from(gameVersions) + .where( + and( + eq(gameVersions.gameId, game.id), + eq(gameVersions.isLatest, true), + ), + ) + .limit(1) + + // If no latest version, get the most recent one + const version = + latestVersion ?? + (await db + .select({ + id: gameVersions.id, + versionString: gameVersions.versionString, + buildId: gameVersions.buildId, + isLatest: gameVersions.isLatest, + createdAt: gameVersions.createdAt, + }) + .from(gameVersions) + .where(eq(gameVersions.gameId, game.id)) + .orderBy(gameVersions.createdAt) + .limit(1) + .then((rows) => rows[0] ?? null)) + + return { + game: { + ...game, + steamAppId: game.steamAppId ?? null, + }, + version, + } + }, + { + query: t.Object({ + steamAppId: t.Numeric(), + }), + detail: { + description: + "Look up a game and its latest version by Steam App ID. Used by the DeckyVault Decky Loader plugin to resolve game info before importing benchmarks.", + }, + }, +) \ No newline at end of file diff --git a/lib/api/index.ts b/lib/api/index.ts index c2d90da..e2c481c 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -30,3 +30,5 @@ export { dashboardPublicRoutes } from "./dashboard-public" export { cronRoutes } from "./cron" export { profilePhotoRoutes } from "./profile-photo" export { mobileRoutes } from "./mobile" +export { gamesLookupRoutes } from "./games-lookup" +export { performanceImportRoutes } from "./performance-import" diff --git a/lib/api/performance-import.ts b/lib/api/performance-import.ts new file mode 100644 index 0000000..a59aad5 --- /dev/null +++ b/lib/api/performance-import.ts @@ -0,0 +1,284 @@ +import { Elysia, t } from "elysia" +import { db } from "@/lib/db/index" +import { + performanceEntries, + gameVersions, + games, + hardware, + gamePlatformSupport, +} from "@/lib/db/schema" +import { eq, and } from "drizzle-orm" +import { requireAuthWithApiKeyFallback } from "@/lib/auth/api-key-guard" +import { recalculatePlayability } from "./playability" + +const VALID_UPSCALER_TYPES = ["none", "fsr", "dlss", "xess", "lsfg", "other"] as const +const VALID_FRAME_GEN_METHODS = ["none", "fsr_fg", "dlss_fg", "lsfg", "other"] as const +const VALID_ANTICHEAT_STATUSES = ["none", "supported", "unsupported", "unknown"] as const + +type UpscalerType = (typeof VALID_UPSCALER_TYPES)[number] +type FrameGenMethod = (typeof VALID_FRAME_GEN_METHODS)[number] +type AntiCheatStatus = (typeof VALID_ANTICHEAT_STATUSES)[number] + +export const performanceImportRoutes = new Elysia({ + prefix: "/performance", + detail: { tags: ["Performance"] }, +}).post( + "/import", + async ({ body, request, set }) => { + // ── Auth: session or API key ────────────────────────────────── + const guard = await requireAuthWithApiKeyFallback(request.headers) + if (!guard.ok) { + set.status = guard.status + return { error: guard.error } + } + + // ── Validate version (1 only for now) ───────────────────────── + if (body.version !== 1) { + set.status = 400 + return { error: "Unsupported import format version" } + } + + // ── Resolve game version from steamAppId ────────────────────── + const steamAppId = body.steamAppId + if (!steamAppId) { + set.status = 400 + return { error: "steamAppId is required" } + } + + const [game] = await db + .select({ id: games.id }) + .from(games) + .where(eq(games.steamAppId, steamAppId)) + .limit(1) + + if (!game) { + set.status = 404 + return { + error: `No game found with steamAppId ${steamAppId}. Submit the game on DeckyVault first.`, + } + } + + // Find the latest version, or create one if needed + let [version] = await db + .select({ id: gameVersions.id }) + .from(gameVersions) + .where( + and( + eq(gameVersions.gameId, game.id), + eq(gameVersions.isLatest, true), + ), + ) + .limit(1) + + if (!version) { + // Get the most recent version + const [existing] = await db + .select({ id: gameVersions.id }) + .from(gameVersions) + .where(eq(gameVersions.gameId, game.id)) + .orderBy(gameVersions.createdAt) + .limit(1) + + if (existing) { + version = existing + } else { + // Create a stub version so we can create the entry + const [newVersion] = await db + .insert(gameVersions) + .values({ + gameId: game.id, + isLatest: true, + }) + .returning({ id: gameVersions.id }) + version = newVersion + } + } + + // ── Validate hardware ───────────────────────────────────────── + const hardwareSlug = body.hardwareSlug + const [device] = await db + .select({ slug: hardware.slug, deviceType: hardware.deviceType }) + .from(hardware) + .where(eq(hardware.slug, hardwareSlug)) + .limit(1) + + if (!device) { + set.status = 400 + return { + error: `Unknown hardware slug: "${hardwareSlug}". Available devices: see /api/hardware`, + } + } + + // ── Validate FPS fields ─────────────────────────────────────── + const fpsAvg = Number(body.fpsAvg) + if (isNaN(fpsAvg) || fpsAvg < 1 || fpsAvg > 500) { + set.status = 400 + return { error: "fpsAvg must be between 1 and 500" } + } + + const fpsLow = body.fpsLow != null ? Number(body.fpsLow) : null + if (fpsLow !== null && (isNaN(fpsLow) || fpsLow < 0 || fpsLow > 500)) { + set.status = 400 + return { error: "fpsLow must be between 0 and 500" } + } + + const fpsOnePercentLow = + body.fpsOnePercentLow != null ? Number(body.fpsOnePercentLow) : null + if ( + fpsOnePercentLow !== null && + (isNaN(fpsOnePercentLow) || fpsOnePercentLow < 0 || fpsOnePercentLow > 500) + ) { + set.status = 400 + return { error: "fpsOnePercentLow must be between 0 and 500" } + } + + const fpsHigh = body.fpsHigh != null ? Number(body.fpsHigh) : null + if (fpsHigh !== null && (isNaN(fpsHigh) || fpsHigh < 0 || fpsHigh > 500)) { + set.status = 400 + return { error: "fpsHigh must be between 0 and 500" } + } + + // ── Validate enums ──────────────────────────────────────────── + const rawUpscalerType = body.upscalerType ?? "none" + const upscalerType: UpscalerType = VALID_UPSCALER_TYPES.includes( + rawUpscalerType as UpscalerType, + ) + ? (rawUpscalerType as UpscalerType) + : "none" + + const rawFrameGenMethod = body.frameGenMethod ?? "none" + const frameGenMethod: FrameGenMethod = VALID_FRAME_GEN_METHODS.includes( + rawFrameGenMethod as FrameGenMethod, + ) + ? (rawFrameGenMethod as FrameGenMethod) + : "none" + + // ── Validate other numeric fields ───────────────────────────── + const tdpWatts = body.tdpWatts != null ? Number(body.tdpWatts) : null + if (tdpWatts !== null && (isNaN(tdpWatts) || tdpWatts <= 0)) { + set.status = 400 + return { error: "tdpWatts must be greater than 0" } + } + + const loadTimeSsd = body.loadTimeSsd != null ? Number(body.loadTimeSsd) : null + const loadTimeSd = body.loadTimeSd != null ? Number(body.loadTimeSd) : null + + // ── Validate userNotes length ───────────────────────────────── + const userNotes = body.userNotes ?? null + if (userNotes && typeof userNotes === "string" && userNotes.length > 5000) { + set.status = 400 + return { error: "userNotes must be 5000 characters or less" } + } + + // ── Create the performance entry ────────────────────────────── + const [entry] = await db + .insert(performanceEntries) + .values({ + versionId: version.id, + hardwareSlug, + userId: guard.user.id, + fpsAvg, + fpsLow, + fpsOnePercentLow, + fpsHigh, + protonVersion: body.protonVersion ?? null, + osVersion: body.osVersion ?? null, + upscalerType, + upscalerVersion: body.upscalerVersion ?? null, + frameGenMethod, + loadTimeSsd, + loadTimeSd, + tdpWatts, + launchOptions: body.launchOptions ?? null, + settingsJson: body.settingsJson ?? null, + userNotes, + customSystem: body.customSystem ?? false, + }) + .returning() + + // ── Update / create gamePlatformSupport ────────────────────── + const [existingSupport] = await db + .select() + .from(gamePlatformSupport) + .where( + and( + eq(gamePlatformSupport.gameId, game.id), + eq(gamePlatformSupport.hardwareSlug, hardwareSlug), + ), + ) + .limit(1) + + if (existingSupport) { + await db + .update(gamePlatformSupport) + .set({ + antiCheatRelevant: + body.antiCheatRelevant ?? existingSupport.antiCheatRelevant, + antiCheatName: body.antiCheatRelevant + ? (body.antiCheatName ?? existingSupport.antiCheatName) + : null, + antiCheatStatus: (body.antiCheatStatus ?? + existingSupport.antiCheatStatus) as AntiCheatStatus, + updatedAt: new Date(), + }) + .where(eq(gamePlatformSupport.id, existingSupport.id)) + } else { + await db.insert(gamePlatformSupport).values({ + gameId: game.id, + hardwareSlug, + isSupported: true, + protonStatus: "unknown", + antiCheatRelevant: body.antiCheatRelevant ?? false, + antiCheatName: body.antiCheatRelevant ? (body.antiCheatName ?? null) : null, + antiCheatStatus: (body.antiCheatStatus ?? "unknown") as AntiCheatStatus, + playabilityStatus: "unknown", + }) + } + + // Fire-and-forget playability recalculation + recalculatePlayability(game.id).catch((err) => + console.error("Failed to recalculate playability:", err), + ) + + set.status = 201 + return { + id: entry.id, + gameId: game.id, + versionId: version.id, + createdAt: entry.createdAt.toISOString(), + authMethod: guard.keyId ? "api-key" : "session", + } + }, + { + body: t.Object({ + version: t.Number(), + steamAppId: t.Number(), + hardwareSlug: t.String(), + fpsAvg: t.Number(), + fpsLow: t.Optional(t.Nullable(t.Number())), + fpsOnePercentLow: t.Optional(t.Nullable(t.Number())), + fpsHigh: t.Optional(t.Nullable(t.Number())), + protonVersion: t.Optional(t.Nullable(t.String())), + osVersion: t.Optional(t.Nullable(t.String())), + upscalerType: t.Optional(t.String()), + upscalerVersion: t.Optional(t.Nullable(t.String())), + frameGenMethod: t.Optional(t.String()), + tdpWatts: t.Optional(t.Nullable(t.Number())), + loadTimeSsd: t.Optional(t.Nullable(t.Number())), + loadTimeSd: t.Optional(t.Nullable(t.Number())), + launchOptions: t.Optional(t.Nullable(t.String())), + settingsJson: t.Optional(t.Nullable(t.Any())), + userNotes: t.Optional(t.Nullable(t.String())), + customSystem: t.Optional(t.Boolean()), + antiCheatRelevant: t.Optional(t.Boolean()), + antiCheatName: t.Optional(t.Nullable(t.String())), + antiCheatStatus: t.Optional(t.String()), + }), + detail: { + description: + "Import a performance benchmark from a DeckyVault plugin export (.deckyvault.json). " + + "Accepts either session cookies or an x-api-key header for authentication. " + + "The steamAppId is used to resolve the game and its latest version automatically.", + }, + }, +) \ No newline at end of file diff --git a/lib/auth-client.ts b/lib/auth-client.ts index 0055432..70580c9 100644 --- a/lib/auth-client.ts +++ b/lib/auth-client.ts @@ -1,6 +1,7 @@ import { createAuthClient } from 'better-auth/react' import { adminClient, emailOTPClient, lastLoginMethodClient } from 'better-auth/client/plugins' import { passkeyClient } from '@better-auth/passkey/client' +import { apiKeyClient } from '@better-auth/api-key/client' export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL || "https://localhost:3000", @@ -8,7 +9,8 @@ export const authClient = createAuthClient({ emailOTPClient(), passkeyClient(), lastLoginMethodClient(), - adminClient() + adminClient(), + apiKeyClient(), ] }) diff --git a/lib/auth.ts b/lib/auth.ts index 83daae1..340e8df 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -2,6 +2,7 @@ import { betterAuth } from 'better-auth' import { admin, captcha, emailOTP, lastLoginMethod } from 'better-auth/plugins' import { passkey } from '@better-auth/passkey' import { expo } from '@better-auth/expo' +import { apiKey } from '@better-auth/api-key' import { drizzleAdapter } from '@better-auth/drizzle-adapter' import { db } from '@/lib/db/index' import { ac, admin as adminRole, moderator, contributor, user } from '@/lib/auth/permissions' @@ -72,6 +73,19 @@ export const auth = betterAuth({ defaultRole: 'user', adminRoles: ['admin'], }), + apiKey({ + defaultPrefix: 'dv_', + requireName: true, + keyExpiration: { + defaultExpiresIn: null, + disableCustomExpiresTime: false, + }, + rateLimit: { + enabled: true, + timeWindow: 1000 * 60 * 60, // 1 hour + maxRequests: 1000, + }, + }), expo(), ], socialProviders: { diff --git a/lib/auth/api-key-guard.ts b/lib/auth/api-key-guard.ts new file mode 100644 index 0000000..84cbccd --- /dev/null +++ b/lib/auth/api-key-guard.ts @@ -0,0 +1,98 @@ +import { auth } from "@/lib/auth" +import { db } from "@/lib/db/index" +import { user } from "@/lib/db/schema/auth" +import { eq } from "drizzle-orm" +import type { Session } from "better-auth" + +type User = typeof auth.$Infer.Session.user + +type ApiKeyGuardResult = + | { ok: true; user: User; session: Session | null; keyId: string } + | { ok: false; error: string; status: number } + +/** + * Attempts to authenticate a request using an API key from the x-api-key header. + * Verifies the key via Better Auth and looks up the user from the database. + */ +export async function authenticateWithApiKey( + requestHeaders: Headers, +): Promise { + const apiKey = requestHeaders.get("x-api-key") + if (!apiKey) { + return { ok: false, error: "Missing x-api-key header", status: 401 } + } + + try { + const result = await auth.api.verifyApiKey({ + body: { + key: apiKey, + }, + }) + + if (!result.valid || !result.key) { + const errorMessage = String(result.error?.message ?? "Invalid API key") + return { ok: false, error: errorMessage, status: 401 } + } + + const userId = result.key.referenceId + const keyId = result.key.id + + // Look up the user directly from the database + const [dbUser] = await db + .select() + .from(user) + .where(eq(user.id, userId)) + .limit(1) + + if (!dbUser) { + return { ok: false, error: "User not found for API key", status: 401 } + } + + // Build a minimal user object matching Better Auth's Session.user type + const authedUser: User = { + id: dbUser.id, + name: dbUser.name, + email: dbUser.email, + emailVerified: dbUser.emailVerified, + image: dbUser.image, + createdAt: dbUser.createdAt, + updatedAt: dbUser.updatedAt, + role: dbUser.role ?? "user", + banned: dbUser.banned ?? null, + banReason: dbUser.banReason ?? null, + banExpires: dbUser.banExpires ?? null, + } + + return { + ok: true, + user: authedUser, + session: null, + keyId, + } + } catch (err) { + console.error("[api-key-guard] API key verification failed:", err) + return { ok: false, error: "API key verification failed", status: 500 } + } +} + +/** + * Combined auth guard: first tries session auth (cookie), then falls back + * to API key auth (x-api-key header). Returns the authenticated user. + */ +export async function requireAuthWithApiKeyFallback( + requestHeaders: Headers, +): Promise { + // Try session auth first + const session = await auth.api.getSession({ headers: requestHeaders }) + if (session) { + return { + ok: true, + user: session.user, + session: session.session, + keyId: "", + } + } + + // Fall back to API key + return authenticateWithApiKey(requestHeaders) +} \ No newline at end of file diff --git a/lib/db/schema/auth.ts b/lib/db/schema/auth.ts index cda957e..2b1b8cc 100644 --- a/lib/db/schema/auth.ts +++ b/lib/db/schema/auth.ts @@ -135,3 +135,46 @@ export const passkeyRelations = relations(passkey, ({ one }) => ({ references: [user.id], }), })) + +export const apikey = pgTable( + "apikey", + { + id: text("id").primaryKey(), + configId: text("config_id").notNull().default("default"), + name: text("name"), + start: text("start"), + referenceId: text("reference_id").notNull(), + prefix: text("prefix"), + key: text("key").notNull(), + refillInterval: integer("refill_interval"), + refillAmount: integer("refill_amount"), + lastRefillAt: timestamp("last_refill_at"), + enabled: boolean("enabled").default(true).notNull(), + rateLimitEnabled: boolean("rate_limit_enabled").default(true).notNull(), + rateLimitTimeWindow: integer("rate_limit_time_window"), + rateLimitMax: integer("rate_limit_max"), + requestCount: integer("request_count").default(0).notNull(), + remaining: integer("remaining"), + lastRequest: timestamp("last_request"), + expiresAt: timestamp("expires_at"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), + permissions: text("permissions"), + metadata: text("metadata"), + }, + (table) => [ + index("apikey_config_id_idx").on(table.configId), + index("apikey_reference_id_idx").on(table.referenceId), + index("apikey_key_idx").on(table.key), + ], +) + +export const apikeyRelations = relations(apikey, ({ one }) => ({ + user: one(user, { + fields: [apikey.referenceId], + references: [user.id], + }), +})) diff --git a/package.json b/package.json index 8ded1e8..9392c39 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1036.0", + "@better-auth/api-key": "^1.6.22", "@better-auth/drizzle-adapter": "^1.6.9", "@better-auth/expo": "^1.6.11", "@better-auth/passkey": "^1.6.9",