Skip to content

Feat/ci pipeline clean - #1

Open
Midoriya-w wants to merge 46 commits into
mainfrom
feat/ci-pipeline-clean
Open

Feat/ci pipeline clean#1
Midoriya-w wants to merge 46 commits into
mainfrom
feat/ci-pipeline-clean

Conversation

@Midoriya-w

@Midoriya-w Midoriya-w commented May 27, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #


Type of Change

  • Bug fix
  • New feature
  • Refactor (no functional change)
  • UI / Design change
  • Tests only
  • Documentation
  • Infrastructure / DevOps
  • Security

What Changed


How to Test


Checklist

  • My code follows the project's coding style (pnpm -r run lint passes).
  • TypeScript compiles without errors (pnpm -r run typecheck).
  • I have added or updated tests for the changes I made.
  • All tests pass locally (pnpm -r run test).
  • I have updated documentation where necessary.
  • No new console.log or debug statements left in the code.
  • Breaking changes are documented in this PR description.

Screenshots / Recordings


Additional Context

Summary by CodeRabbit

Release Notes

  • New Features

    • Event management system with create, join, and attendee tracking
    • WebView-based OAuth flows for streamlined platform follows
    • Follow action logging for analytics and auditing
    • NFC NDEF payload generation for card sharing
    • Copy profile link button with feedback
    • Profile search/lookup functionality
    • Rate limiting for API protection
  • Bug Fixes

    • Enhanced OAuth security with CSRF protection and state validation
    • Link ownership verification prevents unauthorized modifications
    • Improved error handling for concurrent profile updates
  • Documentation

    • Architecture guide for hybrid follow engine
    • Security setup instructions for environment variables
    • Contributors and project support sections
  • Style

    • Premium profile card redesign
    • Loading placeholders and empty state components

Review Change Stack

amritbej and others added 30 commits May 18, 2026 20:47
Co-authored-by: Amrit <amrit@example.com>
Replace the default browser scrollbar with a custom themed one that
matches DevCard's brand gradient. CSS-only, no JavaScript.

- WebKit/Chromium/Safari: gradient thumb (primary -> accent), themed
  track, hover state with glow.
- Firefox: scrollbar-width thin + scrollbar-color using the same theme
  variables (solid thumb fallback since gradients aren't supported).
- Reuses existing --primary/--accent/--bg-secondary/--primary-glow
  CSS variables so light/dark mode just work.

Closes Dev-Card#151.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

* Improve mobile UI/UX responsiveness and layout consistency

* Remove unnecessary package-lock.json
…d#150)

* chore(backend): add and configure ESLint for backend workspace

* fix(backend): align Prisma versions
… logging (Dev-Card#172)

reply.statusCode defaults to 200 before any response is sent, so the
previous check always evaluated to true and logged failed follows as
success. followGitHub now returns { success, response } so the caller
can log based on the actual API outcome.

Closes Dev-Card#148
…nt APIs (Dev-Card#139)

* docs: add Discord community invitation link to README and CONTRIBUTING.md

* git commit -m "feat(events-api): implement event management REST API with Prisma models"

* fix: revert changes to align with repository tech stack

* fix: Revert changes

* fix: add location field to schema and update API, validation, and tests

* fix: remove accidental schema.prisma file

* fix: Updated schema with location in event
…nagement & deep-link fallback (Dev-Card#177)

* feat: Layer 2 WebView Follow Engine — LinkedIn In-App Connect + Session Management

* fix: resolve TypeScript compilation issues and restore settings navigation

* feat: WebView LinkedIn Connect Engine + Follow system (Section 6.9)

- Backend: followRoutes returns webview strategy for LinkedIn/Twitter platforms
- Backend: POST /api/follow/:platform/:targetUsername/log for telemetry
- Backend: DELETE /api/follow/:platform/:targetUsername/log to reset Done state
- Backend: public profile now returns followed:true for previously connected links
- Backend: auth improvements — encode mobile redirect URI in OAuth state
- Mobile: WebViewScreen — full LinkedIn JS injection engine with polling,
  MutationObserver, visibilitychange, popstate, and injectedJSBeforeContentLoaded
- Mobile: DevCardViewScreen — premium UI, emoji icons, brand-colored buttons,
  Done tile with long-press reset, GitHub browser fallback
- Mobile: HomeScreen — username search bar to view any DevCard profile
- Mobile: App.tsx — hash fragment token extraction for OAuth deep links
- Mobile: config.ts — auto-detects LAN IP via Expo Constants for Expo Go
- Mobile: Expo migration — index.js, metro.config.js, babel.config.js, app.json
- Tests: new follow.test.ts cases for webview strategy and log endpoint
- Docs: README updated with telemetry and fallback overlay details
- Config: docker-compose port 5433, .env.example LAN IP placeholders

* fix: address PR review comments from Harxhit

- prisma.ts: replace authenticate:any with proper typed signature
  (request: FastifyRequest, reply: FastifyReply) => Promise<void>
- auth.ts: replace err as any with instanceof Error check in both
  GitHub and Google OAuth catch blocks for type-safe error handling
- Skeleton.tsx: replace width/height as any with DimensionValue type
  from react-native to preserve TypeScript safety

* fix: address remaining PR review comments from Harxhit

- connect.ts: replace err as any with instanceof Error check in
  GitHub connect catch block (same pattern as auth.ts fix)
- MainTabs.tsx: extract WebViewConnect params into standalone exported
  type WebViewConnectParams for reusability and future maintainability
- profiles.test.ts: replace mockPrisma as any with Pick<PrismaClient,'user'>
  and unknown cast to preserve TypeScript safety in tests
Signed-off-by: Parth Patidar <parth11.patidar@gmail.com>
* feat: add context-card diffing utility and validation layer

* feat: add NFC tag payload generation endpoint with card ownership validation

* fix: add Zod query validation and improve error handling in NFC route

* fix: resolve merge conflicts in app.ts

* fix: add typed response schema NfcPayloadResponse

* fix: remove typo in import statement in cards.ts

* refactor: narrow try catch scope in NFC payload route
…ate (Dev-Card#211)

randomBytes was used in generateState() without being imported from
crypto, causing a ReferenceError crash on any GET /connect/github request.
Also renamed parseGoogleState to parseOAuthState since the function is
exclusively used in the GitHub connect flow — Google connect does not
exist in this file.

Closes Dev-Card#178

Signed-off-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
Co-authored-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
…v-Card#144)

* fix(auth): encrypt OAuth tokens using encryption utility directly

auth.ts silently stored GitHub OAuth access tokens as plaintext because
the encryption check relied on a non-existent `app.encryption` Fastify
decorator - the condition always evaluated false, falling back to the raw
token. connect.ts called `app.encryption.encrypt()` directly, throwing
a TypeError at runtime and breaking the GitHub connect flow entirely.

Both routes now import `encrypt()` directly from utils/encryption.ts,
consistent with how follow.ts already imports `decrypt()` from the same module.

* fix(auth): isolate OAuth token persistence with focused try/catch

Wrap the encrypt + oAuthToken.upsert block in its own try/catch so that
a transient DB failure during token storage does not abort the login flow.
The platform token is supplementary -- authentication (JWT issuance) proceeds
even when persistence fails, and the error is logged for observability.

Addresses reviewer feedback on PR Dev-Card#144.

---------

Signed-off-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
Co-authored-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
…ard#171)

Following PA instructions merge conflicts are fixed.
…ard#228)

* fix: resolve ESLint issues in apps/backend/src/routes/cards.ts

* chore: remove local .eslintrc.json

---------

Signed-off-by: Krish Kumar <anuragbraveboy@gmail.com>
Co-authored-by: anuragbraveboy-sudo <krishnyk229@gmail.com>
…ev-Card#157)

* feat: improve card UI in light mode with better shadows and spacing

* style: improve card spacing and add smooth hover shadow
Co-authored-by: Prashantkumar Khatri <prashantkhatri202@gmail.com>
* fix: improve error handling in public.ts

* chore: remove unrelated frontend changes

* fix: improve typing and standardized error handling in public.ts

* chore: remove unrelated frontend changes

* fix: use shared getErrorMessage utility
…ation (Dev-Card#229)

All five route handlers in eventRoutes defined absolute /api/events* paths
while app.ts also registered the plugin with prefix: '/api/events'. Fastify
concatenates registration prefix and route path, producing double-prefixed
endpoints (/api/events/api/events, /api/events/api/events/:slug, etc.) that
are unreachable in production.

Strip the /api/events prefix from every route definition so paths are
relative (/, /:slug, /:slug/join, /:slug/leave, /:slug/attendees),
consistent with every other route plugin in the codebase.

Update the test buildApp() to register with { prefix: '/api/events' },
matching production. Inject URLs in existing tests already use the full
/api/events/* paths and require no changes.

Fixes Dev-Card#224.
Srejoye and others added 16 commits May 23, 2026 21:49
…ovements (Dev-Card#261)

Signed-off-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
Co-authored-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
…ev-Card#272)

The /:username/qr endpoint accepted an unbounded ?size= query parameter.
An unauthenticated caller could request an arbitrarily large raster
(e.g. size=99999999) and trigger an out-of-memory condition in the QR
rasteriser before any DB lookup or auth check.

Changes:
- Add MIN_QR_SIZE (1) and MAX_QR_SIZE (2048) constants
- Parse size with parseInt() and reject NaN or out-of-range values with
  400 before touching the database or allocating any image buffers
- Wrap QR generation in try/catch; propagate generation failures as 500
  instead of crashing the process
- Add regression tests covering: boundary values, NaN input, negative
  values, extreme values, missing param, SVG format, unknown user, and
  a generation-failure path

Signed-off-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
Co-authored-by: Prashantkumar Khatri <96608160+ShantKhatri@users.noreply.github.com>
…e /dev-login route issued a valid 30-day JWT for the demo user withzero authentication. It is now only registered when NODE_ENV is not'production', preventing unauthenticated access on deployed instances.Fixes Dev-Card#247 (Dev-Card#282)
… /api/connect/github OAuth flow generated a nonce but never storedor verified it, allowing an attacker to forge a state parameter andattach a GitHub token to an arbitrary user account.Fix:- On initiation, store the nonce in Redis as oauth:nonce:<nonce> with the userId as value and a 10-minute TTL- On callback, look up the nonce in Redis and reject if missing or if the stored userId does not match the decoded state- Consume (delete) the nonce after verification — one-time use onlyFixes Dev-Card#248 (Dev-Card#283)
* fix: standardize error handling in follow route

* fix: resolve implicit any in event attendees mapping

* fix: resolve implicit any in attendee map with Prisma type

---------

Signed-off-by: Pari Maheshwari <parimaheshwari777@gmail.com>
…ard loader (Dev-Card#218) (Dev-Card#257)

The server load function for /devcard/[id] was fetching card data from
a hardcoded http://localhost:3000 URL, causing the route to fail silently
in all non-local environments (staging, production, Docker).

Replace the hardcoded URL with the BACKEND_URL environment variable,
falling back to http://localhost:3000 for local development. This matches
the existing pattern used by the /u/[username] route.

Also improve error handling: wrap the fetch in a try/catch to handle
network-level failures with a proper 500 response, distinguish 404 (card
not found) from other backend errors, and re-throw SvelteKit HttpError
objects so they are not swallowed by the catch block.
…ions (Dev-Card#252) (Dev-Card#289)

* fix(backend): automatically handle default card reassignment on card deletion

* feat(shared): add platform-specific regex validation for card handles

* fix(backend): catch unhandled errors in card endpoints

* refactor(backend): add typed responses and unhandled exception handling per review

* feat(backend): implement centralized DB error handling for Prisma exceptions
…-Card#271)

* fix(profiles): handle P2002 on concurrent username claims

The username update handler performs a read-before-write uniqueness
check (findFirst -> update). Under concurrent requests, both callers
can pass the read, race to write, and have Prisma throw P2002 on the
losing write — propagating as an unhandled 500.

Wrap user.update in a targeted try/catch: P2002 maps to a deterministic
409 Conflict with the same "Username already taken" message the pre-check
already returns. Other errors are logged and returned as 500 unchanged.

The existing findFirst read is preserved as a fast-path that avoids
hitting the write path for clearly taken usernames. The DB unique
constraint remains the authoritative guard against the race.

Add tests covering:
- P2002 on user.update (concurrent race simulation) -> 409
- unexpected DB errors on user.update -> 500
- no findFirst call when no username is in the payload

Fixes Dev-Card#227.

* refactor(profiles): add explicit ProfileUpdateResponse type to PUT /me

Addresses maintainer feedback requesting typed responses.

Previously the PUT /me handler returned `updated` typed implicitly through
Prisma's deep generic inference (Prisma.UserGetPayload<...>), making the
response contract invisible without tracing through generated types.

Changes:
- Declare `ProfileUpdateResponse` at module scope, following the explicit
  response-type convention already used in public.ts
- Type the `user.update` result variable as `ProfileUpdateResponse` so the
  ten-field response contract is visible at the call site
- Return the named variable rather than the raw Prisma result

No logic changes.  All 8 tests continue to pass.
…deletion (Dev-Card#285)

* fix(backend): automatically handle default card reassignment on card deletion

* feat(backend): add limit to card list query

* refactor(backend): add try-catch and typed responses per review

* feat(backend): add Fastify request schemas for card routes

* feat(backend): use Fastify typed request schema generics for card routes

* feat(backend): remove manual JSON request schemas, keeping Fastify generic typing
…Dev-Card#186) (Dev-Card#208)

The jwt plugin was registered with a hard-coded fallback:

    secret: process.env.JWT_SECRET || 'dev-secret-change-me'

Because the fallback string is committed to the public repository, any
attacker could sign arbitrary JWTs for any userId and gain full
authenticated access to every protected API endpoint.

Changes:

  utils/validateEnv.ts (new)
    Exports a synchronous validateEnv() function that checks JWT_SECRET
    and ENCRYPTION_KEY before the Fastify instance is created.  Missing
    or empty values trigger an immediate process.exit(1).  In production
    (NODE_ENV=production), JWT_SECRET is also compared against the set of
    known insecure defaults shipped in the repository; a match is treated
    as a hard failure.  All errors are collected and printed in a single
    exit so operators can fix everything in one deploy cycle.  Secret
    values are never written to any output.

  app.ts
    Calls validateEnv() as the very first statement of buildApp(), before
    the Fastify instance is instantiated and before any plugin is
    registered.  This guarantees that no partially-initialised auth state
    can exist: if validation fails, JWT is never configured.  The
    now-redundant fallback is removed; process.env.JWT_SECRET! is used
    instead (the non-null assertion is safe because validateEnv() exits
    the process before returning when the value is absent).

  __tests__/validateEnv.test.ts (new)
    11 focused tests covering: absent JWT_SECRET, empty JWT_SECRET,
    insecure default in production, insecure default allowed in dev/test,
    absent ENCRYPTION_KEY, empty ENCRYPTION_KEY, multi-secret failure
    (single exit call), happy-path in dev and production, and a check
    that secret values are never surfaced in console output.
Dev-Card#183)

POST /api/cards and PUT /api/cards/:id accepted arbitrary platformLink IDs
without verifying they belong to the authenticated user. Because platformLink
IDs are exposed in the public profile API, any authenticated user could attach
another user's verified social links to their own card, enabling impersonation.

Add a pre-flight ownership check before each CardLink write. A single indexed
query confirms every requested ID exists with userId = current user. If the
count does not match, the request is rejected with 403 before any write occurs.

Covered by new tests in src/__tests__/cards.test.ts.
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces event management APIs, hardens OAuth with CSRF protection, redesigns mobile screens with unified loading and empty states, adds a sophisticated LinkedIn WebView connector for follow flows, and comprehensively refreshes web app styling and theme variables. Changes span backend route handlers, database schema, mobile UI components, web CSS, and shared validation utilities.

Changes

Backend Infrastructure & Core Services

Layer / File(s) Summary
Environment validation and startup
.env.example, apps/backend/src/utils/validateEnv.ts, apps/backend/src/__tests__/validateEnv.test.ts
JWT_SECRET and ENCRYPTION_KEY are now validated at app startup via validateEnv(), with production safeguards against known insecure defaults and comprehensive test coverage preventing secret leakage.
Prisma schema: Event and EventAttendee models
apps/backend/prisma/schema.prisma
New Event and EventAttendee models with relationships to User for event organization and attendance tracking, including composite uniqueness constraints.
Error handling utilities
apps/backend/src/utils/error.util.ts
Shared getErrorMessage and handleDbError utilities normalize errors and map Prisma codes (P2002, P2025, etc.) to appropriate HTTP responses with structured logging.
ESLint configuration and CI workflows
apps/backend/eslint.config.js, apps/backend/package.json, .github/workflows/ci.yml, .github/workflows/pr-title.yml
TypeScript-aware ESLint flat config with plugin overrides for tests and scripts; CI workflow runs typecheck/lint/test on Node 18 & 20 with coverage upload; PR title validation enforces conventional commits.
Fastify app initialization
apps/backend/src/app.ts, apps/backend/src/plugins/prisma.ts
App now validates environment at startup, registers rate limiting (100 req/min), skips DB plugins in test mode, registers NFC routes, and simplifies health response to { status: 'ok' }.

Backend Features & API Routes

Layer / File(s) Summary
Event lifecycle endpoints and tests
apps/backend/src/routes/event.ts, apps/backend/src/__tests__/event.test.ts, apps/backend/src/validations/event.validation.ts
New event CRUD endpoints: POST creates with normalized slug deduplication, GET retrieves with attendee count, POST/DELETE join/leave with unique constraint handling, GET attendees with paginated public profiles. Comprehensive tests cover auth, validation, edge cases.
Card management with ownership checks and atomic updates
apps/backend/src/routes/cards.ts, apps/backend/src/__tests__/cards.test.ts
Enhanced card routes enforce link ownership before writes (403 if foreign), use Prisma $transaction for atomic operations, prevent deletion of last card, and promote alternate default on default-card deletion.
OAuth and CSRF protection enhancements
apps/backend/src/routes/auth.ts, apps/backend/src/routes/connect.ts
Add CSRF via signed cookies and Redis nonces; use crypto.randomBytes for strong state entropy; implement non-production /dev-login; update mobile redirects to URL fragments; log structured error messages.
Follow endpoint with webview strategy and logging
apps/backend/src/routes/follow.ts, apps/backend/src/__tests__/follow.test.ts
Add platform-configured webview follow strategy (returns URL for LinkedIn); refactor API follow with switch over platform; add POST/DELETE for follow log lifecycle; refactor followGitHub to return structured result.
Public routes with rate limiting and follow tracking
apps/backend/src/routes/public.ts, apps/backend/src/__tests__/public.test.ts
Add rate limiting; implement soft auth (attempt JWT without failing); track followedLinkIds per viewer and include followed boolean in responses; tighten QR size validation and wrap generation in try/catch.
Profile updates, NFC routes, and platform validation
apps/backend/src/routes/profiles.ts, apps/backend/src/routes/nfc.ts, apps/backend/src/utils/validators.ts, packages/shared/src/platforms.ts
Profile PUT /me wraps in try/catch, maps Prisma P2002 to 409; NFC GET /payload validates card ownership and builds URL; extend createLinkSchema with platform-specific regex validation for github/linkedin/twitter.

Mobile App: UI Components, Config, and WebView Connector

Layer / File(s) Summary
Shared UI components
apps/mobile/src/components/EmptyState.tsx, apps/mobile/src/components/LoadingPlaceholder.tsx, apps/mobile/src/components/Skeleton.tsx
New EmptyState component with optional emoji/description; LoadingPlaceholder with configurable rows; Skeleton updated to use DimensionValue type.
Mobile config, Expo migration, and navigation types
apps/mobile/index.js, apps/mobile/metro.config.js, apps/mobile/babel.config.js, apps/mobile/src/config.ts, apps/mobile/src/navigation/MainTabs.tsx, apps/mobile/package.json, apps/mobile/app.json
Switch to Expo (registerRootComponent, getDefaultConfig, worklets plugin); dynamic dev server host via expo-constants; Linking.createURL for OAUTH_REDIRECT_URI; add WebViewConnectParams type; update dependencies to latest versions.
Screen loading and empty states across all screens
apps/mobile/src/screens/CardsScreen.tsx, apps/mobile/src/screens/ConnectPlatformsScreen.tsx, apps/mobile/src/screens/HomeScreen.tsx, apps/mobile/src/screens/LinksScreen.tsx, apps/mobile/src/screens/ScanScreen.tsx, apps/mobile/src/screens/SettingsScreen.tsx, apps/mobile/src/screens/ViewsScreen.tsx
Add loading state management to all screens; replace ActivityIndicator with LoadingPlaceholder or skeleton rows; replace inline empty UI with shared EmptyState component; simplify error handlers.
DevCardViewScreen and WebViewScreen enhanced flow
apps/mobile/src/screens/DevCardViewScreen.tsx, apps/mobile/src/screens/WebViewScreen.tsx, apps/mobile/App.tsx
DevCardViewScreen adds platform emoji mapping, memoized fetchProfile with auth, POST webview follow, followSuccessLinkId param handling, reset handler, dynamic button colors. WebViewScreen becomes full connector: LinkedIn DOM polling, injected JS, fallback overlay, progress tracking, success detection, backend logging, auto-dismiss. Deep-link parsing supports URL hash tokens.

Web App Styling & Design Overhaul

Layer / File(s) Summary
CSS theme variables and global styling
apps/web/src/app.css, apps/web/src/app.html
Updated color palette, radial-gradient backgrounds, shadows, borders, transition timings; increase heading line-height; glass backdrop blur 18px; theme-aware scrollbar; reduced-motion media query; new utility classes (premium-card, card-wrapper, page-container, brand-text); explicit button styling.
Landing, DevCard, and profile page layouts
apps/web/src/routes/+page.svelte, apps/web/src/routes/devcard/[id]/+page.svelte, apps/web/src/routes/devcard/[id]/+page.server.ts, apps/web/src/routes/u/[username]/+page.svelte
Landing: nav spacing/theme-toggle, hero badge/headline/CTA, features grid with responsive breakpoints. DevCard: premium card styling, larger avatar, clamped headline, action tiles. Profile: add copy-link with Clipboard API fallback, background gradient, card styling. Use process.env.BACKEND_URL for API base.

Shared Utilities & Documentation

Layer / File(s) Summary
Card validation and diff utilities
packages/shared/src/cards.ts, packages/shared/src/__tests__/cards.test.ts, packages/shared/src/index.ts
Add CardValidationResult type, PLATFORMS allowlist, validateCardPlatforms (checks empty/unknown/duplicate/overflow), diffCardPlatforms (added/removed/unchanged); full test coverage.
Docker and documentation
docker-compose.yml, apps/backend/README.md, README.md
Update postgres port to 5433; add backend README detailing hybrid follow engine and LinkedIn WebView layer; update root README with env secret generation commands and contributors/support sections.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Backend
  participant Prisma
  participant LinkedIn
  Client->>Backend: POST /api/events (create event)
  Backend->>Prisma: create with normalized slug
  Prisma-->>Backend: event created
  Backend-->>Client: 201 event details
  Client->>Backend: POST /api/events/:slug/join
  Backend->>Prisma: create eventAttendee
  Prisma-->>Backend: attendee record
  Backend-->>Client: 201 joined
  Client->>Backend: GET /api/follow/linkedin/:username
  Backend-->>Client: 200 { strategy: 'webview', url: 'linkedin.com/in/...' }
  Client->>Client: Navigate WebView to URL
  Client->>LinkedIn: Load LinkedIn page
  LinkedIn-->>Client: Page loaded
  Client->>Client: Inject JS to poll DOM / detect success
  Client->>Backend: POST /api/follow/linkedin/:username/log { status: 'success' }
  Backend-->>Client: 200 { logId, status }
  Client-->>Client: Auto-dismiss and return to profile
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A hop through events and OAuth chains,
With WebView windows crossing LinkedIn domains,
Loading states and shadows dance so fine,
Cards now guard their links—a trust design.
Validation reigns and tests parade,
DevCard's shine in grand cascade!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ci-pipeline-clean

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
apps/web/src/routes/devcard/[id]/+page.svelte (1)

18-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden external navigation opened with _blank.

Line 19 opens untrusted URLs without noopener,noreferrer, which exposes window.opener and enables reverse-tabnabbing.

Security fix
 function handlePlatformClick(link: any) {
-  window.open(link.url, '_blank');
+  window.open(link.url, '_blank', 'noopener,noreferrer');
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/routes/devcard/`[id]/+page.svelte around lines 18 - 20,
handlePlatformClick currently calls window.open(link.url, '_blank') exposing
window.opener; change it to open with noreferrer/noopener and nullify opener:
call window.open(link.url, '_blank', 'noopener,noreferrer') and then, after
opening, if the returned window object exists set newWindow.opener = null; keep
the function name handlePlatformClick and ensure you handle a possible null
return from window.open.
apps/backend/src/utils/validators.ts (1)

22-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unknown platform IDs in createLinkSchema.

platform currently accepts any non-empty string, and unknown IDs skip validationRegex. This allows invalid platform records through validation.

💡 Suggested fix
 export const createLinkSchema = z.object({
-  platform: z.string().min(1),
+  platform: z.string().min(1).refine((value) => Boolean(getPlatform(value)), {
+    message: 'Unsupported platform',
+  }),
   username: z.string().min(1).max(200),
   url: z.string().url().optional(),
 }).superRefine((data, ctx) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/utils/validators.ts` around lines 22 - 29, createLinkSchema
currently allows any non-empty platform string and simply skips validation when
getPlatform(data.platform) returns undefined; change the superRefine logic in
createLinkSchema to explicitly reject unknown platform IDs by checking
getPlatform(data.platform) and using ctx.addIssue (or ctx.addIssue with code:
"custom") to add a validation error if platformDef is falsy (e.g., "unknown
platform id"), otherwise continue to apply platformDef.validationRegex against
data.username as before; reference the createLinkSchema definition, the
superRefine callback, getPlatform, and platformDef.validationRegex when making
this change.
apps/backend/src/routes/connect.ts (1)

138-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix unreachable mobile redirect branch in GitHub connect callback

In apps/backend/src/routes/connect.ts, decodedState.nonce comes from generateState() which returns randomBytes(32).toString('hex') (64-char hex), so it can never start with 'mobile_'. The branch at lines 138-140 is therefore dead code. The mobile redirect mechanism exists in apps/backend/src/routes/auth.ts via clientState.startsWith('mobile_')/buildOAuthState, but the /api/connect/github flow in connect.ts doesn’t include any mobile indicator in its state/nonce. Remove the branch or align the connect flow’s state structure with the auth flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/connect.ts` around lines 138 - 140, The branch in
connect.ts that checks decodedState.nonce.startsWith('mobile_') is unreachable
because generateState() produces a 64-char hex nonce that never starts with
'mobile_'; either remove this dead branch or make the /api/connect/github flow
use the same mobile-indicating state format as auth.ts (which uses
clientState.startsWith('mobile_') / buildOAuthState). Fix by choosing one
approach: (A) delete the mobile redirect branch in the callback handler that
references decodedState.nonce and its redirect to MOBILE_REDIRECT_URI, or (B)
change the connect flow to accept/parse the combined state (use
buildOAuthState/clientState semantics) so decodedState contains the 'mobile_'
prefix check and the mobile redirect logic works; update references to
generateState(), decodedState.nonce, buildOAuthState, and clientState
accordingly.
apps/mobile/src/screens/HomeScreen.tsx (1)

49-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid forcing full-screen loading during pull-to-refresh.

On Line 50, fetchData always sets loading to true, so onRefresh can replace the screen with the skeleton instead of keeping in-place refresh behavior. Consider a showLoading flag (like in CardsScreen) and pass false from onRefresh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/screens/HomeScreen.tsx` around lines 49 - 83, fetchData
currently always calls setLoading(true), causing full-screen skeleton to appear
during pull-to-refresh; change fetchData to accept an optional showLoading
(default true) or read a local flag (like showLoading used in CardsScreen) and
only call setLoading(true) when showLoading is true, then update onRefresh to
call fetchData(false) (or set the flag to false) so only setRefreshing is used
for pull-to-refresh; update references to fetchData, onRefresh, setLoading and
setRefreshing accordingly and ensure finally still clears setLoading only when
it was set.
apps/mobile/src/screens/DevCardViewScreen.tsx (1)

10-10: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix Clipboard import for React Native 0.81.5

apps/mobile/src/screens/DevCardViewScreen.tsx imports Clipboard from react-native (line 10) and calls Clipboard.setString(...) (line 180), but RN 0.81.5 doesn’t export Clipboard; apps/mobile/package.json also lacks @react-native-clipboard/clipboard. Add the dependency and switch the import.

Proposed fix
 import {
   View,
   Text,
   StyleSheet,
   ScrollView,
   TouchableOpacity,
   Image,
   Linking,
-  Clipboard,
   StatusBar,
   ActivityIndicator,
   Alert,
 } from 'react-native';
+import Clipboard from '`@react-native-clipboard/clipboard`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/screens/DevCardViewScreen.tsx` at line 10,
DevCardViewScreen.tsx currently imports Clipboard from 'react-native' and uses
Clipboard.setString(...) but RN 0.81.5 no longer exports Clipboard; add the
external package `@react-native-clipboard/clipboard` to apps/mobile/package.json
(and run yarn/npm install) and update the import in DevCardViewScreen.tsx to
import Clipboard from '`@react-native-clipboard/clipboard`' (leaving the call
sites like Clipboard.setString(...) unchanged); ensure the new dependency is
saved to package.json so CI/dev environments install it.
🟡 Minor comments (10)
apps/backend/src/routes/cards.ts-218-232 (1)

218-232: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Default reassignment and deletion are not atomic.

If deleting the default card, the promotion of the oldest remaining card (lines 225-228) and the actual deletion (line 232) happen in separate operations. A concurrent delete of oldestRemainingCard or a failure between these steps could leave the user without a default card. Wrap both in a $transaction for consistency.

🔒 Proposed fix to use a transaction
       if (existing.isDefault) {
         const oldestRemainingCard = await app.prisma.card.findFirst({
           where: { userId, id: { not: id } },
           orderBy: { createdAt: 'asc' },
         });

         if (oldestRemainingCard) {
-          await app.prisma.card.update({
-            where: { id: oldestRemainingCard.id },
-            data: { isDefault: true },
-          });
+          await app.prisma.$transaction(async (tx) => {
+            await tx.card.update({
+              where: { id: oldestRemainingCard.id },
+              data: { isDefault: true },
+            });
+            await tx.card.delete({ where: { id } });
+          });
+          reply.status(204).send();
+          return;
         }
       }

       await app.prisma.card.delete({ where: { id } });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/cards.ts` around lines 218 - 232, When deleting a
card that isDefault (existing.isDefault) wrap the promotion and deletion in a
single Prisma transaction: replace the separate calls to
app.prisma.card.findFirst, app.prisma.card.update, and app.prisma.card.delete
with a single app.prisma.$transaction that (1) finds the oldest remaining card
excluding id, (2) if found updates that card to isDefault: true, and (3) deletes
the target card; ensure you use the same id and userId values inside the
transaction so both operations are atomic and cannot leave the user without a
default card.
apps/web/src/routes/+page.svelte-31-35 (1)

31-35: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace placeholder social URLs with real production URLs.

Line 31 and Line 32 still point to devcard.example.com, so social sharing metadata will be incorrect in production previews.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/routes/`+page.svelte around lines 31 - 35, The OG and Twitter
meta tags in the +page.svelte component still use placeholder
devcard.example.com URLs (og:url, og:image, twitter:image); update those values
to the real production domain and image paths used in production (replace
"https://devcard.example.com/" and "https://devcard.example.com/og-image.jpg"
with the actual production URL and og image URL) so social sharing metadata is
correct (look for meta tags with property="og:url", property="og:image",
name="twitter:image", etc.).
apps/backend/src/routes/public.ts-338-347 (1)

338-347: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Enforce strict integer parsing for QR size

parseInt(rawSize, 10) accepts partial numeric prefixes (e.g., parseInt("400.5", 10) === 400 and parseInt("400abc", 10) === 400), so inputs with fractional/non-numeric suffixes can still pass Number.isInteger and the min/max bounds check. Validate the query string format before converting.

💡 Suggested fix
-    const rawSize = (request.query as any).size;
-    const size = rawSize !== undefined ? parseInt(rawSize, 10) : 400;
-
-    if (!Number.isInteger(size) || size < MIN_QR_SIZE || size > MAX_QR_SIZE) {
+    const rawSize = (request.query as any).size;
+    if (rawSize !== undefined && !/^\d+$/.test(rawSize)) {
+      return reply.status(400).send({
+        error: `QR size must be an integer between ${MIN_QR_SIZE} and ${MAX_QR_SIZE}`,
+      });
+    }
+    const size = rawSize !== undefined ? Number(rawSize) : 400;
+
+    if (!Number.isInteger(size) || size < MIN_QR_SIZE || size > MAX_QR_SIZE) {
       return reply.status(400).send({
         error: `QR size must be an integer between ${MIN_QR_SIZE} and ${MAX_QR_SIZE}`,
       });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/public.ts` around lines 338 - 347, The size parsing
currently uses parseInt on rawSize which accepts partial numeric strings like
"400.5" or "400abc"; change the logic to strictly validate the query value
before parsing: check (rawSize = (request.query as any).size) if it's undefined
then use default 400, otherwise ensure it's a string matching only digits (e.g.
/^\d+$/) so fractional or suffixed values are rejected, then convert to an
integer and apply the existing bounds check against MIN_QR_SIZE and MAX_QR_SIZE;
return the same 400 reply when validation fails. Reference symbols: rawSize,
size, MIN_QR_SIZE, MAX_QR_SIZE, request.query, reply.
apps/backend/src/routes/event.ts-226-226 (1)

226-226: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

204 No Content must not include a response body.

HTTP 204 status code indicates "No Content" and should have an empty body. Sending {message: 'User left'} violates the HTTP specification.

📋 Proposed fix
-          return reply.status(204).send({message: 'User left'})
+          return reply.status(204).send()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/event.ts` at line 226, The handler currently returns
reply.status(204).send({message: 'User left'}) which violates HTTP spec because
204 responses must not include a body; update the response so it either sends an
empty 204 (e.g., reply.status(204).send() or reply.code(204).send()) or change
the status to 200/202 if you need to include a message (e.g.,
reply.status(200).send({message: 'User left'})); locate and update the line with
reply.status(204).send({message: 'User left'}) in the leave/exit route handler
in event.ts.
apps/backend/src/routes/event.ts-115-117 (1)

115-117: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include error details in log message.

The error object is caught but not logged, making debugging difficult.

📝 Proposed fix
       } catch (error) {
-          app.log.error('Failed to create event'); 
+          app.log.error({ err: error }, 'Failed to create event'); 
           return reply.status(500).send({error: 'Failed to create event'})
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/event.ts` around lines 115 - 117, The catch block
currently calls app.log.error('Failed to create event') without the caught
error; update the error handling in the create event route to include the caught
error details by passing the error (or error.stack) to app.log.error and include
enough context in the message (e.g., "Failed to create event:") while keeping
the existing reply.status(500).send({ error: 'Failed to create event' })
behavior; locate the catch that uses app.log.error and reply.status(500).send
and modify that call to log the error object.
apps/backend/src/routes/event.ts-187-192 (1)

187-192: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use proper error typing and include error details in logs.

The error:any type annotation bypasses type safety, and the error log on line 191 doesn't include the actual error details.

🔍 Proposed fix
-      } catch (error:any) {
+      } catch (error) {
           if(error.code === "P2002" ){
               return reply.status(409).send({error: 'Already joined'})
           }
-          app.log.error((error as Error).message); 
+          app.log.error({ err: error }, 'Failed to join event');
           return reply.status(500).send({error: 'Failed to join'})
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/event.ts` around lines 187 - 192, Change the catch
parameter from any to unknown and narrow it before using: check if the error is
a Prisma.PrismaClientKnownRequestError (import Prisma) and test error.code ===
"P2002" to return 409; otherwise narrow to Error (instanceof Error) or include
the raw object in structured logs and send a 500. Replace app.log.error((error
as Error).message) with a structured log that includes the full error object
(e.g., app.log.error({ error }, 'Failed to join')) so the actual error details
are recorded while preserving type-safety in the catch block where you reference
error.code, reply, and app.log.
apps/backend/src/routes/event.ts-227-232 (1)

227-232: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use proper error typing and include error details in logs.

Same issues as the join endpoint: error:any bypasses type safety, and error details aren't logged.

🔍 Proposed fix
-      } catch (error:any) {
+      } catch (error) {
           if(error.code === 'P2025'){
               return reply.status(404).send({error: 'User not found'})
           }
-          app.log.error((error as Error).message)
+          app.log.error({ err: error }, 'Failed to leave event')
           return reply.status(500).send({error: 'Failed to leave'})
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/event.ts` around lines 227 - 232, The catch block
currently types the exception as any and only logs the message string; change
catch(error:any) to catch(error: unknown) and narrow-to-type before checking
error.code (e.g., guard with error instanceof
Prisma.PrismaClientKnownRequestError or a type predicate) so the P2025 check is
safe, and replace the simple app.log.error((error as Error).message) with a
structured log that includes the full error object (for example app.log.error({
error }, 'Failed to leave') or similar) before returning
reply.status(500).send({ error: 'Failed to leave' }) so both type safety and
full error details are preserved; adjust references in this handler (the catch
block that uses app.log.error, reply.status and the P2025 check) accordingly.
apps/backend/src/routes/auth.ts-62-64 (1)

62-64: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove debug console.log statements in production code.

These OAuth redirect debug logs should use app.log.debug() or be removed entirely. console.log bypasses structured logging and may leak sensitive URL parameters in production environments.

Proposed fix
-  console.log('--- GITHUB OAUTH REDIRECT ---');
-  console.log('URL:', authUrl);
+  app.log.debug({ authUrl }, 'GitHub OAuth redirect');
   return reply.redirect(authUrl);
-  console.log('--- GOOGLE OAUTH REDIRECT ---');
-  console.log('URL:', authUrl);
+  app.log.debug({ authUrl }, 'Google OAuth redirect');
   return reply.redirect(authUrl);

Also applies to: 215-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/auth.ts` around lines 62 - 64, Replace the debug
console.log calls in apps/backend/src/routes/auth.ts with structured logging or
remove them: locate the GitHub OAuth redirect block that logs '--- GITHUB OAUTH
REDIRECT ---' and 'URL:' (the use of authUrl before calling
reply.redirect(authUrl)), and any similar console.log occurrences around lines
where authUrl is used (also referenced at the later block near lines 215-217);
change these to use app.log.debug(...) so sensitive URL params go through
structured logging (or remove the logs entirely) while keeping
reply.redirect(authUrl) intact.
apps/mobile/src/config.ts-13-13 (1)

13-13: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid a developer-specific fallback host.

Line 13 hardcodes 10.155.14.65, which can silently break dev API routing for anyone not on that network. Prefer a neutral/configurable fallback.

Proposed fix
-  return hostUri?.split(':')[0] || '10.155.14.65';
+  return hostUri?.split(':')[0] || 'localhost';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/config.ts` at line 13, Replace the developer-specific
hardcoded fallback IP in apps/mobile/src/config.ts: instead of returning
'10.155.14.65' when hostUri is missing (the expression hostUri?.split(':')[0] ||
'10.155.14.65'), use a neutral/configurable fallback such as
process.env.MOBILE_API_HOST || 'localhost' (or throw a clear error) so
non-developers don’t silently get routed to a private IP; update the return
expression to use the env var and ensure any config docs mention
MOBILE_API_HOST.
apps/mobile/src/screens/WebViewScreen.tsx-427-442 (1)

427-442: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent catch block may hide issues with malformed WebView messages.

The empty catch {} on line 441 swallows JSON parse errors without any logging. If the WebView posts a malformed message, debugging will be difficult since nothing indicates a failure occurred.

Proposed fix: Log parse failures
             } else if (data.status === 'debug') {
               console.log('[WebView JS] ' + data.message);
             }
-          } catch {}
+          } catch (e) {
+            console.warn('[WebView] Failed to parse message:', event.nativeEvent.data, e);
+          }
         }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/screens/WebViewScreen.tsx` around lines 427 - 442, The
onMessage handler swallows JSON parse errors via an empty catch, making
malformed WebView messages invisible; update the onMessage ((event) => { ... })
catch block to log parse failures and the raw event payload (e.g., using
console.error or process logger) and include the error message and
event.nativeEvent.data to aid debugging, while preserving existing behavior (do
not rethrow) so handleSuccess/navigation still work; locate the JSON.parse call
in the onMessage handler and replace the empty catch with a small error log
referencing the caught error and event.nativeEvent.data.
🧹 Nitpick comments (13)
apps/backend/src/routes/cards.ts (2)

65-74: 💤 Low value

Duplicate linkIds may cause false 403 or ordering issues.

If the client sends duplicate IDs (e.g., [linkA, linkA]), findMany returns one record while linkIds.length is two, triggering an incorrect 403. Consider deduplicating before the ownership check or validating uniqueness in the schema.

♻️ Proposed fix to deduplicate linkIds
       if (parsed.data.linkIds.length > 0) {
+        const uniqueLinkIds = [...new Set(parsed.data.linkIds)];
         const ownedLinks = await app.prisma.platformLink.findMany({
-          where: { id: { in: parsed.data.linkIds }, userId },
+          where: { id: { in: uniqueLinkIds }, userId },
           select: { id: true },
         });

-        if (ownedLinks.length !== parsed.data.linkIds.length) {
+        if (ownedLinks.length !== uniqueLinkIds.length) {
           return reply.status(403).send({ error: 'One or more links do not belong to your account' });
         }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/cards.ts` around lines 65 - 74, The ownership check
can falsely 403 when parsed.data.linkIds contains duplicates; before calling
app.prisma.platformLink.findMany use a deduplicated array (e.g., create
uniqueLinkIds from parsed.data.linkIds using a Set while preserving order) and
pass that uniqueLinkIds to platformLink.findMany and to the length comparison;
also ensure any later code that relies on linkIds uses the deduped uniqueLinkIds
(or maps back to the original indices if ordering/duplicates must be preserved).

132-168: 💤 Low value

Title update is not atomic with link updates.

If title is provided alongside linkIds, the title update (line 133-137) happens outside the transaction. If the subsequent link transaction fails, the title change persists. Consider wrapping both updates in a single transaction if atomicity across both fields is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/cards.ts` around lines 132 - 168, The title update
(app.prisma.card.update) is performed outside the link transaction so a failing
link update can leave the title change committed; move the title change into the
same transaction that modifies cardLink entries. Keep the ownership check for
parsed.data.linkIds as-is, then open a single app.prisma.$transaction that runs
card.update({ where: { id }, data: { title: parsed.data.title } }) together with
the existing tx.cardLink.deleteMany(...) and tx.cardLink.createMany(...)
operations (conditionally skip the update or createMany when title or linkIds
are not provided) so both title and links are applied atomically.
apps/backend/src/__tests__/app.test.ts (1)

7-19: ⚡ Quick win

Ensure app cleanup runs even if test fails.

If buildApp(), inject(), or assertions throw, app.close() on line 18 won't execute, potentially leaving resources open and causing test suite issues.

♻️ Wrap in try/finally to guarantee cleanup
   it('should return status ok', async () => {
     const app = await buildApp();
-
-    const res = await app.inject({
-      method: 'GET',
-      url: '/health',
-    });
-
-    expect(res.statusCode).toBe(200);
-    expect(JSON.parse(res.body)).toEqual({ status: 'ok' });
-
-    await app.close();
+    try {
+      const res = await app.inject({
+        method: 'GET',
+        url: '/health',
+      });
+
+      expect(res.statusCode).toBe(200);
+      expect(JSON.parse(res.body)).toEqual({ status: 'ok' });
+    } finally {
+      await app.close();
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/__tests__/app.test.ts` around lines 7 - 19, The test may
leave resources open because app.close() is only called at the end; modify the
test so buildApp() is assigned to a variable declared outside the try and wrap
the interaction/assertions (including app.inject() and expect calls) in a try
block with a finally that awaits app.close(); ensure you guard the finally with
a null/undefined check (if using let app) so you only call await app.close()
when buildApp() succeeded — reference the buildApp(), app.inject({...}), and
app.close() calls.
apps/web/src/app.css (1)

79-87: ⚡ Quick win

Consolidate duplicated global button rules to avoid cascade conflicts.

Line 227 redefines base button styles introduced at Line 85, which makes transition/background behavior order-dependent and harder to reason about across pages. Prefer a single base button rule and keep variant-specific styles in component classes.

Proposed cleanup
 button {
   font: inherit;
+  border-radius: 8px;
+  padding: 10px 16px;
+  background: `#4f46e5`;
+  color: white;
+  transition: transform 0.24s ease, box-shadow 0.24s ease, background-color 0.24s ease, border-color 0.24s ease, color 0.24s ease;
 }
@@
-button {
-  border-radius: 8px;
-  padding: 10px 16px;
-  background: `#4f46e5`;
-  color: white;
-  transition: 0.3s;
-}
-
 button:hover {
   background: `#4338ca`;
 }

Also applies to: 227-237

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app.css` around lines 79 - 87, There are duplicated global
button rules (the plain button selector is defined twice alongside .btn-primary
and .btn-secondary), causing cascade/order issues; consolidate into a single
base rule by merging the font: inherit; and the transition declarations under
one plain "button" selector (or a single combined selector like "button,
.btn-primary, .btn-secondary" only for shared properties), remove the second
standalone "button" block, and keep variant-specific styles confined to
".btn-primary" and ".btn-secondary" so that base behavior (transitions, font) is
defined once and variants only override what they need.
apps/web/src/routes/+page.svelte (1)

230-298: ⚡ Quick win

Remove duplicated .feature-card and mobile blocks to prevent conflicting styles.

The same selector is redefined several times with different hover, padding, border, and shadow values, making final rendering order-dependent and brittle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/routes/`+page.svelte around lines 230 - 298, Multiple duplicated
.feature-card and mobile blocks create conflicting styles; consolidate into a
single .feature-card rule and one mobile `@media` (max-width: 640px) block. Remove
the repeated declarations and merge intended properties (min-height, padding,
border, border-radius, box-shadow, background, transition) into one canonical
.feature-card, and combine hover behavior into a single .feature-card:hover
rule; keep only one .features media block for mobile grid settings and ensure
the mobile-specific .feature-card margin-bottom exists only inside that single
media query.
apps/backend/src/validations/event.validation.ts (2)

12-12: ⚡ Quick win

Remove unused schema export.

joinEventSchema is exported but never used in the routes file. Since the join endpoint requires no request body validation, this schema serves no purpose.

♻️ Proposed fix
-
-export const joinEventSchema = z.object({})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/validations/event.validation.ts` at line 12, Remove the
unused exported schema joinEventSchema from event.validation.ts: delete the
export const joinEventSchema = z.object({}) declaration and any imports/uses of
joinEventSchema elsewhere (e.g., in route handlers or validators) so the module
no longer exports an unused symbol; if the schema was imported somewhere, update
those files to stop importing it or replace with no-op validation since the join
endpoint requires no request body validation.

5-5: 💤 Low value

Consider removing redundant .min(1) on optional field.

Since description is optional, requiring a minimum length of 1 when provided might be overly restrictive. If a user provides an empty string, it will fail validation. Consider either removing the .optional() or the .min(1) based on business requirements.

💭 Options

Option 1: Allow empty descriptions if provided:

-    description: z.string().min(1).optional(), 
+    description: z.string().optional(), 

Option 2: Make description required with minimum length:

-    description: z.string().min(1).optional(), 
+    description: z.string().min(1), 

Option 3: Transform empty strings to undefined:

-    description: z.string().min(1).optional(), 
+    description: z.string().min(1).optional().or(z.literal('').transform(() => undefined)), 
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/validations/event.validation.ts` at line 5, The description
field in the Zod schema currently uses z.string().min(1).optional(), which
rejects empty strings despite being optional; to fix this either remove the
.min(1) so the field becomes z.string().optional() (allowing empty strings) or,
if you want empty strings treated as not-provided, change it to
z.string().optional().transform(s => (s && s.trim() !== '' ? s : undefined)) so
empty/whitespace strings become undefined; update the schema where the
description symbol is defined in event.validation.ts accordingly.
apps/backend/src/__tests__/event.test.ts (1)

2-2: 💤 Low value

Add space after comma in import.

Minor formatting inconsistency.

♻️ Proposed fix
-import Fastify, { FastifyInstance } from 'fastify';
+import Fastify, { FastifyInstance } from 'fastify';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/__tests__/event.test.ts` at line 2, Update the import
statement for Fastify to include a space after the comma (i.e., between the
default import "Fastify" and the named import block "{ FastifyInstance }") so
the line reads with a space after the comma; edit the import at the top of the
test file where "Fastify" and "FastifyInstance" are imported.
apps/backend/src/routes/event.ts (2)

2-2: ⚡ Quick win

Remove unused import.

joinEventSchema is imported but never used in this file.

♻️ Proposed fix
-import { createEventSchema, joinEventSchema} from '../validations/event.validation';
+import { createEventSchema } from '../validations/event.validation';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/event.ts` at line 2, The import statement in event
route currently imports joinEventSchema but it is unused; remove joinEventSchema
from the import list in the line that imports createEventSchema and
joinEventSchema from '../validations/event.validation' (i.e., keep only
createEventSchema) and run a quick search for any remaining references to
joinEventSchema to ensure no other code depends on it.

97-98: ⚡ Quick win

Remove redundant Date conversions.

startDate and endDate are already coerced to Date objects by Zod's z.coerce.date() in the schema validation (line 77). These manual conversions are unnecessary.

♻️ Proposed fix
-      const startDateObj = new Date(startDate); 
-      const endDateObj = new Date(endDate); 

       try {
           const newEvent = await app.prisma.event.create({
               data: {
                   name, 
                   description, 
                   slug: finalSlug, 
                   location: location,
-                  startDate: startDateObj, 
-                  endDate: endDateObj, 
+                  startDate, 
+                  endDate, 
                   isPublic: isPublic ?? true, 
                   organizerId: userId
               }
           })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/event.ts` around lines 97 - 98, Remove the redundant
new Date(...) conversions for startDate and endDate in the handler: they are
already coerced to Date objects by the Zod schema (z.coerce.date()), so delete
the lines that create startDateObj and endDateObj and update any subsequent
usage to reference startDate and endDate directly (e.g., where
startDateObj/endDateObj are used inside the function that handles the event
query).
packages/shared/src/cards.ts (1)

20-21: 💤 Low value

Consider extracting the maximum platform limit as a named constant.

The hardcoded 10 could be exported as MAX_PLATFORMS_PER_CARD for clarity and reuse across validation logic, error messages, and documentation.

♻️ Proposed refactor
+export const MAX_PLATFORMS_PER_CARD = 10;
+
 const PLATFORMS = new Set([
   'github', 'linkedin', 'twitter', 'instagram', 'youtube',
   'twitch', 'discord', 'devto', 'hashnode', 'medium',
   'dribbble', 'behance', 'figma', 'stackoverflow', 'leetcode',
   'codepen', 'replit', 'npm', 'producthunt', 'website',
 ]);

 export function validateCardPlatforms(platforms: string[]): CardValidationResult {
   const errors: string[] = [];

   if (platforms.length === 0) {
     errors.push('At least one platform is required.');
   }

-  if (platforms.length > 10) {
-    errors.push(`Maximum 10 platforms allowed, got ${platforms.length}.`);
+  if (platforms.length > MAX_PLATFORMS_PER_CARD) {
+    errors.push(`Maximum ${MAX_PLATFORMS_PER_CARD} platforms allowed, got ${platforms.length}.`);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/cards.ts` around lines 20 - 21, Extract the magic number
10 into an exported constant named MAX_PLATFORMS_PER_CARD and use it wherever
the platform limit is enforced or reported: replace the literal `10` in the
check (the platforms.length comparison) with MAX_PLATFORMS_PER_CARD and update
the error message (`errors.push(...)`) to reference the constant so the message
becomes dynamic; export the constant from packages/shared/src/cards.ts for reuse
in other validation or documentation code paths.
apps/backend/src/routes/follow.ts (2)

118-119: 💤 Low value

Inconsistent error typing.

Line 118 uses err: any while Line 75 uses err: unknown. Use unknown consistently for type safety.

Proposed fix
-    } catch (err: any) {
+    } catch (err: unknown) {
       app.log.error('Failed to log follow:', err);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/follow.ts` around lines 118 - 119, Change the catch
parameter from err: any to err: unknown in the follow route's catch block, then
narrow it before logging; update the app.log.error call in that catch so it logs
a safe string or the Error object (e.g., using err instanceof Error ? err :
String(err)) to satisfy type-safety while still providing useful output; modify
the catch signature and the app.log.error invocation around that catch block and
ensure any other uses in the same function follow the same pattern.

96-122: ⚡ Quick win

Missing input validation for status and layer parameters.

The log endpoint accepts arbitrary strings for status and layer without validation, which could lead to inconsistent data in the database. Consider validating against allowed values.

Proposed fix
+const VALID_STATUSES = ['success', 'error', 'pending'] as const;
+const VALID_LAYERS = ['api', 'webview', 'manual', 'link'] as const;
+
 app.post('/:platform/:targetUsername/log', async (
   request: FastifyRequest<{
     Params: { platform: string; targetUsername: string };
     Body: { status?: string; layer?: string };
   }>,
   reply: FastifyReply
 ) => {
   const userId = (request.user as any).id;
   const { platform, targetUsername } = request.params;
   const { status = 'success', layer = 'webview' } = request.body || {};

+  if (!VALID_STATUSES.includes(status as any)) {
+    return reply.status(400).send({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` });
+  }
+  if (!VALID_LAYERS.includes(layer as any)) {
+    return reply.status(400).send({ error: `Invalid layer. Must be one of: ${VALID_LAYERS.join(', ')}` });
+  }
+
   try {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/follow.ts` around lines 96 - 122, The POST handler
registered with app.post('/:platform/:targetUsername/log') currently reads
status and layer from request.body without validation; add validation for these
fields (e.g., define allowedStatus = ['success','failure',...] and allowedLayers
= ['webview','mobile',...] ) and check the extracted status and layer values
(from the request.body destructuring) before creating followLog in
app.prisma.followLog.create; if a value is not in the allowed set, return
reply.status(400).send({ error: 'Invalid status' }) or similar for layer. You
can implement this either via Fastify route schema validation or a short manual
check in the route handler (referencing request.body, status, layer, and the
app.post handler) and ensure the error path logs the invalid input and returns
400.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81db0a35-714b-4c78-9775-edef6e79d779

📥 Commits

Reviewing files that changed from the base of the PR and between 4493b31 and dc2d8f0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (62)
  • .env.example
  • .github/workflows/ci.yml
  • .github/workflows/pr-title.yml
  • README.md
  • apps/backend/README.md
  • apps/backend/eslint.config.js
  • apps/backend/package.json
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/__tests__/app.test.ts
  • apps/backend/src/__tests__/cards.test.ts
  • apps/backend/src/__tests__/event.test.ts
  • apps/backend/src/__tests__/follow.test.ts
  • apps/backend/src/__tests__/profiles.test.ts
  • apps/backend/src/__tests__/public.test.ts
  • apps/backend/src/__tests__/validateEnv.test.ts
  • apps/backend/src/app.ts
  • apps/backend/src/plugins/prisma.ts
  • apps/backend/src/routes/auth.ts
  • apps/backend/src/routes/cards.ts
  • apps/backend/src/routes/connect.ts
  • apps/backend/src/routes/event.ts
  • apps/backend/src/routes/follow.ts
  • apps/backend/src/routes/nfc.ts
  • apps/backend/src/routes/profiles.ts
  • apps/backend/src/routes/public.ts
  • apps/backend/src/utils/error.util.ts
  • apps/backend/src/utils/validateEnv.ts
  • apps/backend/src/utils/validators.ts
  • apps/backend/src/validations/event.validation.ts
  • apps/mobile/App.tsx
  • apps/mobile/app.json
  • apps/mobile/babel.config.js
  • apps/mobile/index.js
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/src/components/CardPickerSheet.tsx
  • apps/mobile/src/components/EmptyState.tsx
  • apps/mobile/src/components/LoadingPlaceholder.tsx
  • apps/mobile/src/components/Skeleton.tsx
  • apps/mobile/src/config.ts
  • apps/mobile/src/navigation/MainTabs.tsx
  • apps/mobile/src/screens/CardsScreen.tsx
  • apps/mobile/src/screens/ConnectPlatformsScreen.tsx
  • apps/mobile/src/screens/DevCardViewScreen.tsx
  • apps/mobile/src/screens/HomeScreen.tsx
  • apps/mobile/src/screens/LinksScreen.tsx
  • apps/mobile/src/screens/ScanScreen.tsx
  • apps/mobile/src/screens/SettingsScreen.tsx
  • apps/mobile/src/screens/ViewsScreen.tsx
  • apps/mobile/src/screens/WebViewScreen.tsx
  • apps/web/src/app.css
  • apps/web/src/app.html
  • apps/web/src/routes/+page.svelte
  • apps/web/src/routes/devcard/[id]/+page.server.ts
  • apps/web/src/routes/devcard/[id]/+page.svelte
  • apps/web/src/routes/u/[username]/+page.svelte
  • docker-compose.yml
  • packages/shared/src/__tests__/cards.test.ts
  • packages/shared/src/cards.ts
  • packages/shared/src/index.ts
  • packages/shared/src/platforms.test.ts
  • packages/shared/src/platforms.ts

Comment thread .github/workflows/ci.yml
Comment on lines +9 to +20
jobs:
ci:
name: Typecheck / Lint / Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]

steps:
- name: Checkout code
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la .github/workflows || true
sed -n '1,120p' .github/workflows/ci.yml | nl -ba

Repository: Midoriya-w/DevCard

Length of output: 352


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,200p' .github/workflows/ci.yml | nl -ba

Repository: Midoriya-w/DevCard

Length of output: 105


🏁 Script executed:

#!/bin/bash
set -euo pipefail
awk '{printf "%4d\t%s\n", NR, $0}' .github/workflows/ci.yml | sed -n '1,120p'

Repository: Midoriya-w/DevCard

Length of output: 1365


🌐 Web query:

actions/checkout persist-credentials default true

💡 Result:

In the actions/checkout GitHub Action, the persist-credentials input defaults to true [1][2]. When set to true (the default), the action configures the local Git configuration with the authentication token or SSH key used to fetch the repository [1][3]. This allows subsequent steps in the same job to execute authenticated Git commands using those credentials [1][2]. These credentials are automatically removed during the post-job cleanup process [1][2]. Setting persist-credentials to false is a recommended security practice for workflows that do not require authenticated Git operations (such as git push) after the initial checkout [4]. By disabling this setting, you limit the exposure of the credentials, as they will not be available to subsequent steps in the job, thereby reducing the attack surface in the event of a compromised dependency or script [5][4].

Citations:


🌐 Web query:

GitHub Actions default GITHUB_TOKEN permissions contents read write when permissions not set

💡 Result:

When you do not explicitly set permissions in a GitHub Actions workflow, the GITHUB_TOKEN is assigned default permissions based on the organization or repository settings [1]. 1. Behavior when permissions are not set: If the permissions key is omitted entirely from your workflow file, the GITHUB_TOKEN is granted a set of default permissions configured at the repository, organization, or enterprise level [1][2]. Historically, these defaults have provided a broad range of read/write access to various API endpoints [1]. 2. Understanding the "write" vs "read" scope: - If you use the permissions key to set even one specific permission (e.g., contents: write), all other permissions not explicitly defined in that block are set to none [1]. This is a critical security behavior: explicitly defining any scope "disables" the implicit default for all other scopes [1][3]. - Pull requests from public forks are a special security case: regardless of the default settings or the permissions defined in the workflow, the GITHUB_TOKEN for these runs is always restricted to read-only access to prevent unauthorized write operations [1][3]. 3. Security Recommendation: To adhere to the principle of least privilege, it is strongly recommended to explicitly define the permissions required for every workflow or job [4][1]. By using the permissions key, you can ensure your workflows have only the specific access they need, rather than relying on potentially broader, non-explicit defaults [4][1]. If you find that a workflow fails after adding a permissions block, it is likely because you must explicitly include all necessary scopes (e.g., both contents: read and pull-requests: write) rather than just the one you intended to change [3].

Citations:


🌐 Web query:

actions/upload-artifact required GITHUB_TOKEN permissions actions write

💡 Result:

For actions/upload-artifact, you generally do NOT need to grant GITHUB_TOKEN “actions: write” (or any specific GITHUB_TOKEN permission scope) just to upload artifacts. actions/upload-artifact uses its own internal runtime authentication (not your repository-scoped GITHUB_TOKEN), so tightening the workflow permissions: for GITHUB_TOKEN typically does not control whether the upload works. This is discussed in the upload-artifact repo: the action “relies on a special [undocumented token] … unrelated to GITHUB_TOKEN” [1]. What can matter instead: 1) Workflow/job permissions still apply to what the action does via the GitHub API (for example, if you use other steps that call the API with GITHUB_TOKEN). Use the permissions: key to grant least-privilege access for GITHUB_TOKEN [2][3]. 2) Repository access/authorization for artifact visibility/management: the upload-artifact action indicates UI controls like deleting artifacts appear only for users with write permissions to the repository [4][5]. 3) The artifact upload may fail for filesystem/permission reasons unrelated to GITHUB_TOKEN (e.g., host path permissions) [6]. Practical recommendation (safe baseline): - Set minimal permissions: for GITHUB_TOKEN for the job (only what other steps require). Don’t add actions: write solely for actions/upload-artifact. If you tell me your exact workflow permissions: block and where the upload is happening (PR from fork vs same-repo, etc.), I can suggest the minimal permissions: needed for the rest of your job.

Citations:


🌐 Web query:

actions/cache required GITHUB_TOKEN permissions contents read actions write

💡 Result:

The actions/cache action does not strictly require any specific GITHUB_TOKEN permissions to function for its primary purpose of saving and restoring cache entries [1][2][3]. The action operates using internal mechanisms to interact with GitHub's caching service, which does not necessitate explicit contents or actions permissions from the workflow's GITHUB_TOKEN [1][4]. However, best security practices for GitHub Actions recommend defining an explicit permissions block for all workflows to follow the principle of least privilege [5][6][4]. If a workflow only uses actions/cache and actions/checkout (to fetch the repository), it is standard and secure to set the permissions as follows [4]: permissions: contents: read This configuration provides the necessary read access for actions/checkout while explicitly denying unnecessary write access [4]. Granting contents: write is not required for caching and should be avoided unless the workflow performs other actions that specifically demand it, such as creating releases or pushing commits [7][8][9]. Recent security guidance and incidents highlight the importance of being cautious with permissions, particularly regarding cache usage [10]. While actions/cache itself is not the source of excessive permission requirements, hardening your permissions block helps mitigate the overall impact if other parts of a workflow are compromised [6][4][10]. If you do not define a permissions block, the GITHUB_TOKEN may default to broader permissions depending on your repository or organization settings [6].

Citations:


🌐 Web query:

actions/setup-node cache pnpm permissions GITHUB_TOKEN

💡 Result:

When using the actions/setup-node action to cache pnpm dependencies, you do not need to manually configure GITHUB_TOKEN permissions for the caching mechanism itself [1][2]. The actions/setup-node and actions/cache actions handle cache restoration and saving automatically using the environment's built-in capabilities [1][2]. Regarding permissions in your GitHub Actions workflow: 1. Standard Permissions: It is generally recommended to set at least contents: read at the job or workflow level to ensure the action can check out your code and install dependencies [1][2]. 2. GITHUB_TOKEN: The GITHUB_TOKEN is automatically provided by GitHub Actions and is generally sufficient for standard CI/CD operations [3]. You do not need to grant it special permissions for actions/setup-node caching [1][2]. 3. Security Note: Recent security incidents in May 2026, such as the TanStack supply chain attack, have highlighted risks associated with cache poisoning in GitHub Actions environments [4]. While the actions/setup-node cache itself is a standard tool, organizations are increasingly advised to review workflow permissions, especially those using pull_request_target, to prevent malicious actors from injecting compromised packages or scripts into the cache [4]. Implementation details for pnpm caching with setup-node: - The actions/setup-node action supports cache: 'pnpm' (requires pnpm v6.10+) [1][5]. - You may alternatively use pnpm/action-setup to install and cache pnpm, which is often used in conjunction with actions/setup-node for a complete Node.js environment [6][7]. - Ensure your packageManager or devEngines field in package.json is configured, or explicitly provide the cache: 'pnpm' input to the setup-node action [1][5]. If you encounter permission errors during installation (such as file access issues), these are typically related to how Node.js is installed on the runner (e.g., system-installed vs. user-space via setup-node) rather than GITHUB_TOKEN limitations [8]. Explicitly setting the node-version input in actions/setup-node is the standard fix for these permission issues [8].

Citations:


Harden GITHUB_TOKEN usage with least-privilege permissions and non-persistent checkout credentials.

  • Workflow doesn’t define permissions, so GITHUB_TOKEN uses broader org/repo defaults.
  • actions/checkout@v4 (step at line 19) defaults to persist-credentials: true, leaving credentials in the git config for subsequent steps.
Proposed fix
 name: CI
@@
 on:
@@
+permissions:
+  contents: read
+
 jobs:
   ci:
@@
       - name: Checkout code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 18-19: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 19-19: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 9 - 20, The workflow is leaving
GITHUB_TOKEN with repo-wide defaults and persisting credentials via
actions/checkout@v4; set explicit least-privilege permissions on the ci job
(e.g., permissions: contents: read) and update the Checkout code step
(actions/checkout@v4) to use persist-credentials: false so credentials are not
written to git config; modify the job definition for "ci" and the Checkout step
to apply these changes (refer to the ci job block and the actions/checkout@v4
step).

Comment thread .github/workflows/ci.yml

steps:
- name: Checkout code
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File =="
ls -la "$FILE" || true

echo
echo "== Relevant lines =="
nl -ba "$FILE" | sed -n '1,120p'

Repository: Midoriya-w/DevCard

Length of output: 231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"

echo "== File =="
wc -l "$FILE" || true

echo
echo "== Lines 1-120 (with numbers) =="
awk '{printf "%4d\t%s\n", NR, $0}' "$FILE" | sed -n '1,120p'

echo
echo "== Grep: uses/persist-credentials/permissions =="
rg -n "uses:|persist-credentials:|^permissions:" "$FILE" || true

Repository: Midoriya-w/DevCard

Length of output: 1646


Pin GitHub Actions to commit SHAs and harden CI permissions

  • uses refs are unpinned tag names (actions/checkout@v4, pnpm/action-setup@v3, actions/setup-node@v4, actions/upload-artifact@v4) at lines 19, 22, 27, 45—pin each to a full commit SHA.
  • No workflow/job-level permissions block, and the checkout step at line 19 does not set persist-credentials: false, widening blast radius if a step is compromised.
Proposed fix
-      - name: Checkout code
-        uses: actions/checkout@v4
+      - name: Checkout code
+        uses: actions/checkout@<full-length-commit-sha>
@@
-      - name: Setup pnpm
-        uses: pnpm/action-setup@v3
+      - name: Setup pnpm
+        uses: pnpm/action-setup@<full-length-commit-sha>
@@
-      - name: Setup Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v4
+      - name: Setup Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@<full-length-commit-sha>
@@
-      - name: Upload coverage
-        uses: actions/upload-artifact@v4
+      - name: Upload coverage
+        uses: actions/upload-artifact@<full-length-commit-sha>

Also applies to: 22-22, 27-27, 45-45

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 18-19: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 19-19: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 19, The workflow uses unpinned action refs
(e.g., actions/checkout@v4, pnpm/action-setup@v3, actions/setup-node@v4,
actions/upload-artifact@v4) and lacks least-privilege CI permissions and
checkout credential hardening; update each "uses" ref to the corresponding full
commit SHA for those actions, add a workflow- or job-level permissions block
that restricts token scopes to only what's required, and set
persist-credentials: false on the checkout step (the actions/checkout
invocation) so the GITHUB_TOKEN is not persisted to the workspace.

Comment on lines +7 to +14
jobs:
pr-title:
name: Enforce Conventional Commits
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the workflow file around the referenced lines and also any permissions blocks.
FILE=".github/workflows/pr-title.yml"

echo "=== File: $FILE ==="
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '1,200p'
else
  echo "File not found: $FILE" >&2
  exit 1
fi

Repository: Midoriya-w/DevCard

Length of output: 168


🌐 Web query:

amannn/action-semantic-pull-request v5 required permissions pull-requests read GITHUB_TOKEN

💡 Result:

For the amannn/action-semantic-pull-request action (v5 and subsequent versions), the required GitHub permissions depend on the features you enable [1][2][3]. By default, the action requires read-only access to pull requests [1][2][3]. You should configure your workflow with the following permission: permissions: pull-requests: read However, if you enable the WIP (Work-in-Progress) feature (by setting wip: true), the action requires write access to update the status of the pull request to stay in a "pending" state while the WIP marker is present [1][2][3]. In this case, you must configure the following permission instead: permissions: pull-requests: write The GITHUB_TOKEN is required for both configurations to allow the action to interact with the GitHub API [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/pr-title.yml"
echo "=== Exists? ==="
ls -la "$FILE" || exit 1

echo "=== File contents (with line numbers) ==="
awk '{printf "%5d\t%s\n", NR, $0}' "$FILE" | sed -n '1,200p'

Repository: Midoriya-w/DevCard

Length of output: 590


🌐 Web query:

amannn action-semantic-pull-request v5 default permissions pull-requests read statuses write checks

💡 Result:

For the amannn/action-semantic-pull-request action (v5 and later), the required permissions depend on whether you are using the work-in-progress (WIP) feature [1][2][3]. Standard Configuration If you are not using the WIP feature, the action typically requires only the following permission [4][2][3]: permissions: pull-requests: read WIP Feature Configuration If you enable the WIP feature (wip: true), the action must update the pull request status to keep it in a "pending" state while the title contains "[WIP]" [1][2][3]. This requires elevated permissions [2][3]. While official documentation explicitly emphasizes granting pull-requests: write [1][2][3], many users report that this alone may still result in a "Resource not accessible by integration" error [5][6][7]. Community-verified configurations for successfully using the WIP feature often require both: permissions: pull-requests: write statuses: write The addition of statuses: write is necessary because the action interacts with the GitHub Statuses API to manage the check status when the WIP flag is active [5][7]. If your workflow encounters access errors, explicitly including both permissions is the standard solution [5][6][7].

Citations:


Set explicit least-privilege permissions for the PR title workflow

.github/workflows/pr-title.yml has no permissions: block; for amannn/action-semantic-pull-request@v5 without WIP enabled, least-privilege is permissions: pull-requests: read. Add a workflow- (or job-) level permissions block accordingly; if wip: true is ever enabled, it needs broader permissions (pull-requests: write and typically statuses: write).

🧰 Tools
🪛 zizmor (1.25.2)

[error] 12-12: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-title.yml around lines 7 - 14, Add an explicit
least-privilege permissions block to the workflow or the pr-title job so the
amannn/action-semantic-pull-request@v5 runs with only the needed access: add
permissions: pull-requests: read at workflow- or job-level for the pr-title job;
if you later enable wip: true in that action, update permissions to
pull-requests: write (and typically statuses: write) instead. Ensure the
permissions block is placed alongside jobs or under the pr-title job so GitHub
enforces least privilege for this action.

name: Enforce Conventional Commits
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/pr-title.yml"
echo "== File = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "Missing: $FILE" >&2
  exit 1
fi

# Show the relevant portion around the cited line
nl -ba "$FILE" | sed -n '1,160p'

echo
echo "== Extracted action references =="
rg -n --hidden --no-ignore-vcs "amannn/action-semantic-pull-request@" "$FILE" || true

echo
echo "== Permissions blocks (workflow/job) =="
rg -n --hidden --no-ignore-vcs "^\s*permissions:" "$FILE" || true

Repository: Midoriya-w/DevCard

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/pr-title.yml"
echo "== File = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "Missing: $FILE" >&2
  exit 1
fi

echo
echo "== Show top 120 lines (with numbers) =="
awk '{printf "%6d  %s\n", NR, $0}' "$FILE" | sed -n '1,120p'

echo
echo "== Extracted action references =="
rg -n --hidden --no-ignore-vcs "amannn/action-semantic-pull-request@" "$FILE" || true

echo
echo "== Permissions blocks (workflow/job) =="
rg -n --hidden --no-ignore-vcs "^[[:space:]]*permissions:" "$FILE" || true

echo
echo "== Any job-level permissions usage =="
rg -n --hidden --no-ignore-vcs "permissions:" "$FILE" || true

Repository: Midoriya-w/DevCard

Length of output: 737


Pin amannn/action-semantic-pull-request to a commit SHA and harden GITHUB_TOKEN permissions.

  • Line 12 uses mutable tag amannn/action-semantic-pull-request@v5; pin to a specific commit SHA for supply-chain guarantees.
  • No explicit permissions: block; GITHUB_TOKEN is set in env, so define least-privilege permissions for the job/workflow.
🧰 Tools
🪛 zizmor (1.25.2)

[error] 12-12: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-title.yml at line 12, Replace the mutable tag
amannn/action-semantic-pull-request@v5 with a specific commit SHA (e.g.
amannn/action-semantic-pull-request@<commit-sha>) to pin the action and update
the workflow to declare minimal GITHUB_TOKEN scopes via a permissions: block
(for example permissions: pull-requests: write and contents: read) at the
workflow or job level rather than relying on an implicit env GITHUB_TOKEN;
ensure the change targets the step using amannn/action-semantic-pull-request and
the workflow job that currently sets GITHUB_TOKEN in env so the action still
works under least-privilege.

Comment on lines +27 to +42
files: ['src/**/*.ts'],

languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},

plugins: {
n: pluginN,
'import-x': pluginImportX,
promise: pluginPromise,
security: pluginSecurity,
unicorn: pluginUnicorn,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/backend/eslint.config.js"

echo "== File header =="
head -n 80 "$FILE"

echo "== Relevant section around lines 160-230 =="
nl -ba "$FILE" | sed -n '150,240p'

echo "== Search for plugin usage and registration =="
rg -n "plugins:|security/|unicorn/|n/|import-x|plugin(Security|Unicorn|N|ImportX)|rules:" "$FILE"

Repository: Midoriya-w/DevCard

Length of output: 2922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/backend/eslint.config.js"
# show entire file if small else segments
LINES=$(wc -l < "$FILE")
echo "Total lines: $LINES"
if [ "$LINES" -le 260 ]; then
  nl -ba "$FILE" | sed -n '1,260p'
else
  nl -ba "$FILE" | sed -n '1,120p'
  nl -ba "$FILE" | sed -n '120,240p'
  nl -ba "$FILE" | sed -n '240,400p'
fi

Repository: Midoriya-w/DevCard

Length of output: 140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# quick eslint flat config semantics check by grabbing eslint version from package.json
# and searching for eslint config docs locally not possible; just read package json
if [ -f "package.json" ]; then
  echo "== Root package.json eslint version =="
  node -p "require('./package.json').devDependencies?.eslint || require('./package.json').dependencies?.eslint || 'no-root-eslint-devdep'"
fi
if [ -f "apps/backend/package.json" ]; then
  echo "== Backend package.json eslint version =="
  node -p "require('./apps/backend/package.json').devDependencies?.eslint || require('./apps/backend/package.json').dependencies?.eslint || 'no-backend-eslint-devdep'"
fi

# print eslint.config.js file import statements for plugin definitions
FILE="apps/backend/eslint.config.js"
echo "== Plugin imports =="
nl -ba "$FILE" | sed -n '1,90p'

Repository: Midoriya-w/DevCard

Length of output: 256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/backend/eslint.config.js"

echo "== Print file length =="
wc -l "$FILE"

echo "== Import section (top 80 lines) =="
sed -n '1,80p' "$FILE"

echo "== Show lines 120-201 with line numbers (no nl) =="
awk 'NR>=120 && NR<=201 { printf "%d\t%s\n", NR, $0 }' "$FILE"

echo "== Search for plugin/rule references =="
rg -n "plugins:\s*{|security/|unicorn/|n/|import-x/|promise/|security:|unicorn:|n:|import-x:|promise:" "$FILE"

echo "== Search for flat-config override blocks that mention these rule namespaces =="
rg -n "files:\s*\\[|security/|unicorn/|n/|import-x/|promise/" "$FILE"

Repository: Midoriya-w/DevCard

Length of output: 6454


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/backend/eslint.config.js"

echo "== Lines containing plugin-prefixed rules =="
rg -n "'(security|unicorn|n)/|\"(security|unicorn|n)/" "$FILE"

echo "== Lines containing files globs for overrides =="
rg -n "files:\s*\[" "$FILE"

echo "== Show complete override blocks (lines 175-205) =="
awk 'NR>=165 && NR<=205 { printf "%d\t%s\n", NR, $0 }' "$FILE"

Repository: Midoriya-w/DevCard

Length of output: 2973


🌐 Web query:

ESLint flat config plugins scope per config object rule "Definition for rule was not found" plugin registration matching files

💡 Result:

The error "Definition for rule was not found" in ESLint flat config (eslint.config.js) typically occurs when a rule is referenced in the rules object, but the associated plugin has not been correctly registered or scoped to the files being linted [1][2]. In flat config, plugins are no longer loaded via strings; they must be imported as JavaScript objects and explicitly registered in the plugins object within the same configuration block where they are used [2][3][4]. Key requirements to resolve this error: 1. Registration Scope: When you define files in a config object, any plugins used by that object must also be included in that same object's plugins property [3][5]. If a plugin is registered in one config object but you attempt to use its rules in a different object (or for files not covered by the first object), ESLint will report that the rule definition cannot be found [1][2]. Example of correct registration: import examplePlugin from "eslint-plugin-example"; export default [ { files: ["/*.js"], plugins: { // The key 'example' defines the namespace used in rules example: examplePlugin, }, rules: { // Rule name must match namespace/rule-name "example/my-rule": "error", }, }, ]; 2. Common Causes for the Error: * Missing Plugin Registration: You have included a rule like "plugin/rule-name" but failed to add the plugin object to the plugins property in that specific configuration block [2][3]. * Scope Mismatch: You have split your configuration into multiple objects. If an object contains rules but lacks the plugins property, those rules will fail even if the plugin is registered in a different object earlier in the array [1][2]. Ensure every config object that uses a plugin's rules also defines that plugin [3]. * Namespace Mismatch: The key used in the plugins object (e.g., "example") acts as the namespace. If you use "example/rule" but registered the plugin as "custom", you must use "custom/rule" [2][3]. * Incorrect Export Format: Ensure your plugin correctly exports an object with a rules property. If using CommonJS plugins, ensure that the module is correctly resolved and the rules are accessible [6]. If you are using shareable configurations, remember that you cannot simply use the old extends string syntax [2]. You must import the config object and spread it into your configuration array, ensuring the plugins it requires are properly registered within the context of the files you are linting [2][7][4].

Citations:


Register n, security, and unicorn plugins for non-src override globs

  • In apps/backend/eslint.config.js, plugins are registered only in the files: ['src/**/*.ts'] block, but later override blocks for **/*.test.ts/**/*.spec.ts (uses security/...) and for prisma/**/*.ts + scripts/**/*.ts (uses n/... and unicorn/...) do not register those plugins, which can cause ESLint to fail with missing rule definitions for matches outside src.
  • Details
Proposed fix
 export default tseslint.config(
@@
   ...tseslint.configs.recommendedTypeChecked,
+
+  // Shared plugin registration for all matching flat-config blocks
+  {
+    plugins: {
+      n: pluginN,
+      'import-x': pluginImportX,
+      promise: pluginPromise,
+      security: pluginSecurity,
+      unicorn: pluginUnicorn,
+    },
+  },
@@
   {
     files: ['src/**/*.ts'],
@@
-    plugins: {
-      n: pluginN,
-      'import-x': pluginImportX,
-      promise: pluginPromise,
-      security: pluginSecurity,
-      unicorn: pluginUnicorn,
-    },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/eslint.config.js` around lines 27 - 42, The ESLint config
currently registers plugins only inside the files: ['src/**/*.ts'] block so
overrides for test files and prisma/scripts files can reference undefined rules;
update the config so that plugin registrations for pluginN, pluginSecurity, and
pluginUnicorn are available to those override globs—either move the plugins: {
n: pluginN, security: pluginSecurity, unicorn: pluginUnicorn, 'import-x':
pluginImportX, promise: pluginPromise } up to the shared/top-level config or
explicitly add those three plugins to the override entries that match
'**/*.test.ts'/'**/*.spec.ts' and 'prisma/**/*.ts' + 'scripts/**/*.ts' so rules
that reference n/*, security/*, and unicorn/* resolve.

Comment on lines +15 to +17
const KNOWN_INSECURE_DEFAULTS: ReadonlySet<string> = new Set([
'dev-secret-change-me',
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block the repository placeholder JWT secret in production too.

KNOWN_INSECURE_DEFAULTS only includes dev-secret-change-me, but .env.example uses a different placeholder. Production can start with that committed default unchanged.

Suggested fix
 const KNOWN_INSECURE_DEFAULTS: ReadonlySet<string> = new Set([
   'dev-secret-change-me',
+  'your-super-secret-jwt-key-change-in-production',
 ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const KNOWN_INSECURE_DEFAULTS: ReadonlySet<string> = new Set([
'dev-secret-change-me',
]);
const KNOWN_INSECURE_DEFAULTS: ReadonlySet<string> = new Set([
'dev-secret-change-me',
'your-super-secret-jwt-key-change-in-production',
]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/utils/validateEnv.ts` around lines 15 - 17,
KNOWN_INSECURE_DEFAULTS currently only lists 'dev-secret-change-me' but the
repo's .env.example uses a different placeholder; update the
KNOWN_INSECURE_DEFAULTS ReadonlySet to also include the placeholder JWT secret
string used in .env.example so the validate logic (in validateEnv.ts) will
reject that committed default in production. Locate the KNOWN_INSECURE_DEFAULTS
constant and append the .env.example placeholder value (exact string) to the set
so both known insecure defaults are blocked when checking JWT secret in
production.

Comment on lines +53 to +58
if (!encryptionKey) {
errors.push(
'ENCRYPTION_KEY is not set. Generate a secure value with:\n' +
' node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate ENCRYPTION_KEY format, not just presence.

Current logic accepts any non-empty string, which allows invalid keys past startup and breaks the “deterministic fail-fast” contract.

Suggested fix
   if (!encryptionKey) {
     errors.push(
       'ENCRYPTION_KEY is not set. Generate a secure value with:\n' +
       '    node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
     );
+  } else if (!/^[a-fA-F0-9]{64}$/.test(encryptionKey)) {
+    errors.push(
+      'ENCRYPTION_KEY must be exactly 64 hex characters (32 bytes).',
+    );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!encryptionKey) {
errors.push(
'ENCRYPTION_KEY is not set. Generate a secure value with:\n' +
' node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
);
}
if (!encryptionKey) {
errors.push(
'ENCRYPTION_KEY is not set. Generate a secure value with:\n' +
' node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
);
} else if (!/^[a-fA-F0-9]{64}$/.test(encryptionKey)) {
errors.push(
'ENCRYPTION_KEY must be exactly 64 hex characters (32 bytes).',
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/utils/validateEnv.ts` around lines 53 - 58, The current
check only ensures ENCRYPTION_KEY (encryptionKey) is present; change it to
validate its format too by verifying the value is a 64-character hex string (32
bytes) using a regex like /^[0-9a-fA-F]{64}$/ and push an error if it fails;
update the error message in validateEnv.ts to instruct how to generate a correct
key (same node -e crypto.randomBytes(32).toString('hex') example) so the app
fails fast and deterministically when ENCRYPTION_KEY is malformed.

Comment on lines +14 to +20
import { useNavigation } from '@react-navigation/native';
import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens';
import { useAuth } from '../context/AuthContext';
import { API_BASE_URL } from '../config';

import { useNavigation } from '@react-navigation/native';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove duplicate useNavigation import to prevent compile/lint failure.

Line 14 and Line 19 import the same symbol from the same module. Keep only one import.

Proposed fix
 import { SafeAreaView } from 'react-native-safe-area-context';
 import { useNavigation } from '`@react-navigation/native`';
 import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens';
 import { useAuth } from '../context/AuthContext';
 import { API_BASE_URL } from '../config';
-
-import { useNavigation } from '`@react-navigation/native`';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useNavigation } from '@react-navigation/native';
import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens';
import { useAuth } from '../context/AuthContext';
import { API_BASE_URL } from '../config';
import { useNavigation } from '@react-navigation/native';
import { useNavigation } from '`@react-navigation/native`';
import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens';
import { useAuth } from '../context/AuthContext';
import { API_BASE_URL } from '../config';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mobile/src/screens/SettingsScreen.tsx` around lines 14 - 20, There are
two identical imports of the symbol useNavigation from
'`@react-navigation/native`' in SettingsScreen.tsx; remove the duplicate import
statement (keep a single import of useNavigation) so only one import of
useNavigation exists and the other imports (COLORS, SPACING, FONT_SIZE,
BORDER_RADIUS, useAuth, API_BASE_URL) remain unchanged.

let mounted = $state(false);
let copyMessage = $state('');
let copyStatus = $state<'success' | 'error'>('success');
let copyMessageTimeout: ReturnType<typeof setTimeout>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix inconsistent timeout variable name in copy flow.

Line 40 and Line 42 reference copyTimeout, but only copyMessageTimeout is declared. This breaks the copy-message timer logic.

Correct variable usage
-    clearTimeout(copyTimeout);
+    clearTimeout(copyMessageTimeout);
@@
-    copyTimeout = setTimeout(() => {
+    copyMessageTimeout = setTimeout(() => {
       copyMessage = '';
     }, 3000);

Also applies to: 40-44

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/routes/u/`[username]/+page.svelte at line 20, The timer variable
is inconsistently named: the declaration uses copyMessageTimeout but the copy
flow references copyTimeout, breaking the timer logic; update all references of
copyTimeout to copyMessageTimeout (or rename the declaration to copyTimeout) so
the same identifier is used in the copy handler, clearTimeout call, and
setTimeout assignment (look for usages in the copy handler and any
clearTimeout/setTimeout lines) to restore correct copy-message timer behavior.

Comment on lines +6 to +11
const PLATFORMS = new Set([
'github', 'linkedin', 'twitter', 'instagram', 'youtube',
'twitch', 'discord', 'devto', 'hashnode', 'medium',
'dribbble', 'behance', 'figma', 'stackoverflow', 'leetcode',
'codepen', 'replit', 'npm', 'producthunt', 'website',
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare PLATFORMS constant with exported platform keys in platforms.ts

# Extract platform keys from platforms.ts PLATFORMS object
ast-grep --pattern $'export const PLATFORMS = {
  $$$
}'

# Also check if there's a PlatformType or similar that enumerates keys
rg -nP --type=ts 'type PlatformType|export const PLATFORMS|export type.*Platform' packages/shared/src/platforms.ts -A 20

Repository: Midoriya-w/DevCard

Length of output: 756


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show cards.ts PLATFORMS snippet
echo "=== packages/shared/src/cards.ts (PLATFORMS) ==="
rg -n "const PLATFORMS" packages/shared/src/cards.ts
sed -n '1,120p' packages/shared/src/cards.ts | nl -ba | sed -n '1,80p'

# Extract cards.ts PLATFORMS string literals
echo
echo "=== Extract cards.ts platform names ==="
python3 - <<'PY'
import re, pathlib, json
p = pathlib.Path("packages/shared/src/cards.ts")
txt = p.read_text(encoding="utf-8")
m = re.search(r"const PLATFORMS\s*=\s*new Set\s*\(\s*\[(.*?)\]\s*\)\s*;", txt, re.S)
if not m:
    print("Could not find cards.ts PLATFORMS new Set([...]);")
    raise SystemExit(1)
inner = m.group(1)
names = re.findall(r"'([^']+)'", inner)
print(json.dumps(sorted(set(names)), indent=2))
print("count:", len(set(names)))
PY

# Extract platforms.ts keys from export const PLATFORMS: Record<string, PlatformDef> = { ... }
echo
echo "=== Extract platforms.ts platform keys ==="
python3 - <<'PY'
import re, pathlib, json
p = pathlib.Path("packages/shared/src/platforms.ts")
txt = p.read_text(encoding="utf-8")

m = re.search(r"export\s+const\s+PLATFORMS\s*:\s*Record<\s*string\s*,\s*PlatformDef\s*>\s*=\s*\{(.*?)\}\s*;\s*$", txt, re.S|re.M)
if not m:
    # Fallback: grab object literal starting at export const PLATFORMS and ending at the matching closing brace + semicolon (heuristic)
    # Take from first "export const PLATFORMS" to the first occurrence of "\n};" after it.
    start = txt.find("export const PLATFORMS")
    if start == -1:
        print("Could not find platforms.ts export const PLATFORMS")
        raise SystemExit(1)
    end = txt.find("\n};", start)
    if end == -1:
        print("Could not find end of platforms.ts PLATFORMS object")
        raise SystemExit(1)
    inner = txt[start:end]
else:
    inner = m.group(1)

# keys appear as "<key>: {" at top-level of the object
keys = re.findall(r"^\s*([a-zA-Z0-9_]+)\s*:\s*\{", inner, re.M)
# Keep only likely platform keys (exclude any non-top-level matches if present)
keys = [k for k in keys if k]
print(json.dumps(sorted(set(keys)), indent=2))
print("count:", len(set(keys)))
PY

# Compare sets and report diffs
echo
echo "=== Diff: cards.ts vs platforms.ts ==="
python3 - <<'PY'
import re, pathlib

def get_cards():
    txt = pathlib.Path("packages/shared/src/cards.ts").read_text(encoding="utf-8")
    m = re.search(r"const PLATFORMS\s*=\s*new Set\s*\(\s*\[(.*?)\]\s*\)\s*;", txt, re.S)
    if not m:
        raise RuntimeError("cards.ts PLATFORMS not found")
    inner = m.group(1)
    return set(re.findall(r"'([^']+)'", inner))

def get_platforms():
    txt = pathlib.Path("packages/shared/src/platforms.ts").read_text(encoding="utf-8")
    start = txt.find("export const PLATFORMS")
    if start == -1:
        raise RuntimeError("platforms.ts export const PLATFORMS not found")
    end = txt.find("\n};", start)
    if end == -1:
        raise RuntimeError("platforms.ts PLATFORMS end not found")
    inner = txt[start:end]
    keys = re.findall(r"^\s*([a-zA-Z0-9_]+)\s*:\s*\{", inner, re.M)
    return set([k for k in keys if k])

cards = get_cards()
plats = get_platforms()

missing_in_cards = sorted(plats - cards)
extra_in_cards = sorted(cards - plats)

print("cards.ts count:", len(cards))
print("platforms.ts count:", len(plats))
print("Missing in cards.ts (in platforms.ts but not cards.ts):", missing_in_cards)
print("Extra in cards.ts (in cards.ts but not platforms.ts):", extra_in_cards)
PY

Repository: Midoriya-w/DevCard

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re, pathlib, json

def extract_cards():
    txt = pathlib.Path("packages/shared/src/cards.ts").read_text(encoding="utf-8")
    m = re.search(r"const\s+PLATFORMS\s*=\s*new\s+Set\s*\(\s*\[(.*?)\]\s*\)\s*;", txt, re.S)
    if not m:
        raise RuntimeError("cards.ts PLATFORMS new Set([...]); not found")
    inner = m.group(1)
    return set(re.findall(r"'([^']+)'", inner))

def extract_platforms():
    txt = pathlib.Path("packages/shared/src/platforms.ts").read_text(encoding="utf-8")
    start = txt.find("export const PLATFORMS")
    if start == -1:
        raise RuntimeError("platforms.ts export const PLATFORMS not found")
    end = txt.find("\n};", start)
    if end == -1:
        raise RuntimeError("platforms.ts PLATFORMS end not found (expected '\\n};')")
    inner = txt[start:end]
    # top-level keys: "<key>: {"
    keys = re.findall(r"^\s*([a-zA-Z0-9_]+)\s*:\s*\{", inner, re.M)
    return set(keys)

cards = extract_cards()
plats = extract_platforms()

print("cards.ts count:", len(cards))
print("platforms.ts count:", len(plats))

missing_in_cards = sorted(plats - cards)
extra_in_cards = sorted(cards - plats)

print("Missing in cards.ts (present in platforms.ts):", missing_in_cards)
print("Extra in cards.ts (not present in platforms.ts):", extra_in_cards)
PY

Repository: Midoriya-w/DevCard

Length of output: 395


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "PLATFORMS" packages/shared/src/cards.ts
sed -n '1,140p' packages/shared/src/cards.ts | cat
sed -n '140,260p' packages/shared/src/cards.ts | cat

Repository: Midoriya-w/DevCard

Length of output: 1465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "validateCardPlatforms\(" -S .
rg -n "from ['\"].*/platforms['\"]|from ['\"].*platforms\.ts['\"]|import .*platforms" packages/shared/src
rg -n "PlatformDef|PlatformType|platform.*validationRegex" packages/shared/src

Repository: Midoriya-w/DevCard

Length of output: 1403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect cards tests
echo "=== packages/shared/src/__tests__/cards.test.ts ==="
sed -n '1,120p' packages/shared/src/__tests__/cards.test.ts | cat

# Inspect platforms tests
echo
echo "=== packages/shared/src/platforms.test.ts ==="
sed -n '1,220p' packages/shared/src/platforms.test.ts | cat

# Find usages of getPlatform / getAllPlatforms
echo
echo "=== usages of getPlatform/getAllPlatforms in repo ==="
rg -n "getPlatform\\(|getAllPlatforms\\(" packages/shared/src -S
rg -n "getPlatform\\(|getAllPlatforms\\(" -S packages | head -n 50

Repository: Midoriya-w/DevCard

Length of output: 7779


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show platforms.ts keys (ids) and getProfileUrl behavior
python3 - <<'PY'
import re, pathlib, json
txt = pathlib.Path("packages/shared/src/platforms.ts").read_text(encoding="utf-8")
# extract platform keys
start = txt.find("export const PLATFORMS")
end = txt.find("\n};", start)
inner = txt[start:end]
keys = re.findall(r"^\s*([a-zA-Z0-9_]+)\s*:\s*\{", inner, re.M)
print("platform ids count:", len(keys))
print("platform ids:", sorted(set(keys)))
PY

# Inspect getProfileUrl and related functions
rg -n "function getProfileUrl|function getWebViewUrl|function getDeepLinkUrl|getPlatform\\(" packages/shared/src/platforms.ts
sed -n '240,340p' packages/shared/src/platforms.ts | cat

Repository: Midoriya-w/DevCard

Length of output: 2344


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "validateCardPlatforms\\(" packages -S

Repository: Midoriya-w/DevCard

Length of output: 915


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "validateCardPlatforms" -S packages
sed -n '1,120p' packages/shared/src/index.ts

Repository: Midoriya-w/DevCard

Length of output: 1196


Fix platform validation to use the canonical platforms.ts registry

packages/shared/src/cards.ts hardcodes a PLATFORMS Set that doesn’t match packages/shared/src/platforms.ts (cards: 20 entries, platforms: 17 ids):

  • Present in cards.ts but missing from platforms.ts: behance, codepen, dribbble, figma, instagram, producthunt, replit, twitch, website, youtube
  • Present in platforms.ts but missing from cards.ts: custom, devfolio, email, gitlab, hackerrank, portfolio, telegram

Update validateCardPlatforms to derive allowed ids from platforms.ts (e.g., Object.keys(platforms.PLATFORMS) or via getPlatform) to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/cards.ts` around lines 6 - 11, The hardcoded PLATFORMS
Set in cards.ts is out of sync with the canonical registry in platforms.ts;
replace the hardcoded set and update validateCardPlatforms to derive allowed
platform ids from platforms.ts (for example by importing platforms.PLATFORMS and
using Object.keys(platforms.PLATFORMS) or by calling getPlatform for each
candidate) so validation uses the authoritative list, remove the hardcoded
PLATFORMS constant, update imports (import * as platforms or the getPlatform
helper) and ensure validateCardPlatforms now checks against that derived list
and returns the same results as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.