Feat/ci pipeline clean - #1
Conversation
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>
…d#150) * chore(backend): add and configure ESLint for backend workspace * fix(backend): align Prisma versions
…ev-Card#150)" (Dev-Card#176) This reverts commit d6afc79.
… 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
Co-authored-by: Dhrupad <24155325@kiit.ac.in>
…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.
…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.
📝 WalkthroughWalkthroughThis 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. ChangesBackend Infrastructure & Core Services
Backend Features & API Routes
Mobile App: UI Components, Config, and WebView Connector
Web App Styling & Design Overhaul
Shared Utilities & Documentation
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winHarden external navigation opened with
_blank.Line 19 opens untrusted URLs without
noopener,noreferrer, which exposeswindow.openerand 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 winReject unknown platform IDs in
createLinkSchema.
platformcurrently accepts any non-empty string, and unknown IDs skipvalidationRegex. 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 winFix unreachable mobile redirect branch in GitHub connect callback
In
apps/backend/src/routes/connect.ts,decodedState.noncecomes fromgenerateState()which returnsrandomBytes(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 inapps/backend/src/routes/auth.tsviaclientState.startsWith('mobile_')/buildOAuthState, but the/api/connect/githubflow inconnect.tsdoesn’t include any mobile indicator in itsstate/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 winAvoid forcing full-screen loading during pull-to-refresh.
On Line 50,
fetchDataalways setsloadingtotrue, soonRefreshcan replace the screen with the skeleton instead of keeping in-place refresh behavior. Consider ashowLoadingflag (like in CardsScreen) and passfalsefromonRefresh.🤖 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 winFix
Clipboardimport for React Native 0.81.5
apps/mobile/src/screens/DevCardViewScreen.tsximportsClipboardfromreact-native(line 10) and callsClipboard.setString(...)(line 180), but RN0.81.5doesn’t exportClipboard;apps/mobile/package.jsonalso 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 winDefault 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
oldestRemainingCardor a failure between these steps could leave the user without a default card. Wrap both in a$transactionfor 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 winReplace 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 winEnforce strict integer parsing for QR
size
parseInt(rawSize, 10)accepts partial numeric prefixes (e.g.,parseInt("400.5", 10) === 400andparseInt("400abc", 10) === 400), so inputs with fractional/non-numeric suffixes can still passNumber.isIntegerand 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 win204 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 winInclude 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 winUse proper error typing and include error details in logs.
The
error:anytype 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 winUse proper error typing and include error details in logs.
Same issues as the join endpoint:
error:anybypasses 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 winRemove debug
console.logstatements in production code.These OAuth redirect debug logs should use
app.log.debug()or be removed entirely.console.logbypasses 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 winAvoid 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 winSilent 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 valueDuplicate
linkIdsmay cause false 403 or ordering issues.If the client sends duplicate IDs (e.g.,
[linkA, linkA]),findManyreturns one record whilelinkIds.lengthis 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 valueTitle update is not atomic with link updates.
If
titleis provided alongsidelinkIds, 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 winEnsure 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 winConsolidate duplicated global
buttonrules to avoid cascade conflicts.Line 227 redefines base
buttonstyles introduced at Line 85, which makes transition/background behavior order-dependent and harder to reason about across pages. Prefer a single basebuttonrule 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 winRemove duplicated
.feature-cardand 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 winRemove unused schema export.
joinEventSchemais 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 valueConsider removing redundant
.min(1)on optional field.Since
descriptionis 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 valueAdd 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 winRemove unused import.
joinEventSchemais 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 winRemove redundant Date conversions.
startDateandendDateare already coerced toDateobjects by Zod'sz.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 valueConsider extracting the maximum platform limit as a named constant.
The hardcoded
10could be exported asMAX_PLATFORMS_PER_CARDfor 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 valueInconsistent error typing.
Line 118 uses
err: anywhile Line 75 useserr: unknown. Useunknownconsistently 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 winMissing input validation for
statusandlayerparameters.The log endpoint accepts arbitrary strings for
statusandlayerwithout 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (62)
.env.example.github/workflows/ci.yml.github/workflows/pr-title.ymlREADME.mdapps/backend/README.mdapps/backend/eslint.config.jsapps/backend/package.jsonapps/backend/prisma/schema.prismaapps/backend/src/__tests__/app.test.tsapps/backend/src/__tests__/cards.test.tsapps/backend/src/__tests__/event.test.tsapps/backend/src/__tests__/follow.test.tsapps/backend/src/__tests__/profiles.test.tsapps/backend/src/__tests__/public.test.tsapps/backend/src/__tests__/validateEnv.test.tsapps/backend/src/app.tsapps/backend/src/plugins/prisma.tsapps/backend/src/routes/auth.tsapps/backend/src/routes/cards.tsapps/backend/src/routes/connect.tsapps/backend/src/routes/event.tsapps/backend/src/routes/follow.tsapps/backend/src/routes/nfc.tsapps/backend/src/routes/profiles.tsapps/backend/src/routes/public.tsapps/backend/src/utils/error.util.tsapps/backend/src/utils/validateEnv.tsapps/backend/src/utils/validators.tsapps/backend/src/validations/event.validation.tsapps/mobile/App.tsxapps/mobile/app.jsonapps/mobile/babel.config.jsapps/mobile/index.jsapps/mobile/metro.config.jsapps/mobile/package.jsonapps/mobile/src/components/CardPickerSheet.tsxapps/mobile/src/components/EmptyState.tsxapps/mobile/src/components/LoadingPlaceholder.tsxapps/mobile/src/components/Skeleton.tsxapps/mobile/src/config.tsapps/mobile/src/navigation/MainTabs.tsxapps/mobile/src/screens/CardsScreen.tsxapps/mobile/src/screens/ConnectPlatformsScreen.tsxapps/mobile/src/screens/DevCardViewScreen.tsxapps/mobile/src/screens/HomeScreen.tsxapps/mobile/src/screens/LinksScreen.tsxapps/mobile/src/screens/ScanScreen.tsxapps/mobile/src/screens/SettingsScreen.tsxapps/mobile/src/screens/ViewsScreen.tsxapps/mobile/src/screens/WebViewScreen.tsxapps/web/src/app.cssapps/web/src/app.htmlapps/web/src/routes/+page.svelteapps/web/src/routes/devcard/[id]/+page.server.tsapps/web/src/routes/devcard/[id]/+page.svelteapps/web/src/routes/u/[username]/+page.sveltedocker-compose.ymlpackages/shared/src/__tests__/cards.test.tspackages/shared/src/cards.tspackages/shared/src/index.tspackages/shared/src/platforms.test.tspackages/shared/src/platforms.ts
| 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 | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la .github/workflows || true
sed -n '1,120p' .github/workflows/ci.yml | nl -baRepository: Midoriya-w/DevCard
Length of output: 352
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,200p' .github/workflows/ci.yml | nl -baRepository: 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:
- 1: https://github.com/actions/checkout/?tab=readme-ov-file
- 2: https://github.com/actions/checkout/tree/refs/heads/main
- 3: https://spacelift.io/blog/github-actions-checkout
- 4: Add persist-credentials: false to GitHub Actions checkout steps aquasecurity/trivy#10305
- 5: Remove
persist-credentialsor change the default tofalseactions/checkout#485
🌐 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:
- 1: https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/
- 2: default token permissions should match github actions default github/gh-aw#200
- 3: https://stackoverflow.com/questions/78444367/github-token-permission-in-workflow-changes-to-read-after-setting-it-to-write
- 4: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token
🌐 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:
- 1: Add details on authentication / permissions used during upload actions/upload-artifact#197
- 2: https://github.com/github/docs/blob/main/content/actions/tutorials/authenticate-with-github_token.md
- 3: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token
- 4: https://github.com/actions/upload-artifact?tab=readme-ov-file
- 5: https://github.com/actions/upload-artifact/blob/c7d193f32edcb7bfad88892161225aeda64e9392/README.md
- 6: https://stackoverflow.com/questions/69977574/github-actions-upload-artifact-access-denied-trying-to-upload-a-file-despite
🌐 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:
- 1: https://github.com/actions/cache/
- 2: https://github.com/actions/cache?tab=readme-ov-file
- 3: https://github.com/actions/cache/blob/main/README.md
- 4: Potential fix for code scanning alert no. 52: Workflow does not contain permissions actions/cache#1697
- 5: https://docs.github.com/actions/reference/authentication-in-a-workflow
- 6: https://www.stepsecurity.io/blog/github-token-how-it-works-and-how-to-secure-automatic-github-action-tokens
- 7: https://stackoverflow.com/questions/67389957/what-permissions-does-github-token-require-for-releases-from-a-github-action
- 8: https://stackoverflow.com/questions/72110199/what-is-contents-write-permission-in-github-workflow
- 9: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
- 10: https://www.rescana.com/post/tanstack-npm-supply-chain-attack-detailed-analysis-of-the-may-2026-github-actions-breach-and-multi-ecosystem-impact/
🌐 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:
- 1: https://github.com/actions/setup-node/tree/refs/heads/main
- 2: https://github.com/actions/setup-node
- 3: https://dev.to/akaranjkar08/github-actions-cicd-build-a-complete-nodejs-pipeline-2026-150
- 4: https://www.rescana.com/post/tanstack-npm-supply-chain-attack-detailed-analysis-of-the-may-2026-github-actions-breach-and-multi-ecosystem-impact/
- 5: https://gitea.com/actions/setup-node/src/branch/main/docs/advanced-usage.md
- 6: https://github.com/pnpm/action-setup
- 7: https://pnpm.io/continuous-integration
- 8: Cannot update NPM on Linux runners unless node_version is explicitly set actions/setup-node#1409
Harden GITHUB_TOKEN usage with least-privilege permissions and non-persistent checkout credentials.
- Workflow doesn’t define
permissions, soGITHUB_TOKENuses broader org/repo defaults. actions/checkout@v4(step at line 19) defaults topersist-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).
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 |
There was a problem hiding this comment.
🧩 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" || trueRepository: Midoriya-w/DevCard
Length of output: 1646
Pin GitHub Actions to commit SHAs and harden CI permissions
usesrefs 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
permissionsblock, and the checkout step at line 19 does not setpersist-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.
| 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 }} |
There was a problem hiding this comment.
🧩 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
fiRepository: 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:
- 1: https://github.com/marketplace/actions/semantic-pull-request?version=v5.4.0
- 2: https://github.com/marketplace/actions/semantic-pull-request
- 3: https://github.com/amannn/action-semantic-pull-request
🏁 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:
- 1: amannn/action-semantic-pull-request@v4...v5
- 2: https://github.com/amannn/action-semantic-pull-request
- 3: https://github.com/amannn/action-semantic-pull-request/
- 4: https://github.com/marketplace/actions/semantic-pull-request
- 5: docs: miss permission for statuses: write amannn/action-semantic-pull-request#233
- 6: Resource not accessible by integration amannn/action-semantic-pull-request#173
- 7: Error: Resource not accessible by integration amannn/action-semantic-pull-request#249
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 |
There was a problem hiding this comment.
🧩 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" || trueRepository: 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" || trueRepository: 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_TOKENis set inenv, 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.
| files: ['src/**/*.ts'], | ||
|
|
||
| languageOptions: { | ||
| parserOptions: { | ||
| projectService: true, | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| }, | ||
|
|
||
| plugins: { | ||
| n: pluginN, | ||
| 'import-x': pluginImportX, | ||
| promise: pluginPromise, | ||
| security: pluginSecurity, | ||
| unicorn: pluginUnicorn, | ||
| }, |
There was a problem hiding this comment.
🧩 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'
fiRepository: 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:
- 1: Flat Config - key rules: key X Could not find plugin X eslint/eslint#17485
- 2: https://fixdevs.com/blog/eslint-flat-config-not-working/
- 3: https://eslint.org/docs/latest/use/configure/plugins
- 4: https://eslint.org/docs/latest/use/configure/migration-guide
- 5: https://allalmohamedlamine.medium.com/eslint-flat-config-and-new-system-an-ultimate-deep-dive-2023-46aa151cbf2b
- 6: Bug: CLI option
--plugincannot load CommonJS plugin in flat config mode eslint/eslint#18159 - 7: https://eslint.org/docs/latest/use/configure/configuration-files
Register n, security, and unicorn plugins for non-src override globs
- In
apps/backend/eslint.config.js, plugins are registered only in thefiles: ['src/**/*.ts']block, but later override blocks for**/*.test.ts/**/*.spec.ts(usessecurity/...) and forprisma/**/*.ts+scripts/**/*.ts(usesn/...andunicorn/...) do not register those plugins, which can cause ESLint to fail with missing rule definitions for matches outsidesrc. -
Details
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.
| const KNOWN_INSECURE_DEFAULTS: ReadonlySet<string> = new Set([ | ||
| 'dev-secret-change-me', | ||
| ]); |
There was a problem hiding this comment.
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.
| 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.
| 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\'))"', | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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'; | ||
|
|
There was a problem hiding this comment.
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.
| 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>; |
There was a problem hiding this comment.
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.
| const PLATFORMS = new Set([ | ||
| 'github', 'linkedin', 'twitter', 'instagram', 'youtube', | ||
| 'twitch', 'discord', 'devto', 'hashnode', 'medium', | ||
| 'dribbble', 'behance', 'figma', 'stackoverflow', 'leetcode', | ||
| 'codepen', 'replit', 'npm', 'producthunt', 'website', | ||
| ]); |
There was a problem hiding this comment.
🧩 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 20Repository: 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)
PYRepository: 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)
PYRepository: 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 | catRepository: 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/srcRepository: 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 50Repository: 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 | catRepository: Midoriya-w/DevCard
Length of output: 2344
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "validateCardPlatforms\\(" packages -SRepository: 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.tsRepository: 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.tsbut missing fromplatforms.ts:behance,codepen,dribbble,figma,instagram,producthunt,replit,twitch,website,youtube - Present in
platforms.tsbut missing fromcards.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.
Summary
Closes #
Type of Change
What Changed
How to Test
Checklist
pnpm -r run lintpasses).pnpm -r run typecheck).pnpm -r run test).console.logor debug statements left in the code.Screenshots / Recordings
Additional Context
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Style