[#485] Move Bloom participant flows out of Blade#486
Conversation
Co-authored-by: Madu <madudiop1122@gmail.com>
📝 WalkthroughWalkthroughBlade now redirects participant traffic to BloomKnights, BloomKnights owns the application/dashboard/profile experience, and shared packages provide portal config, auth, and participant API support. Hackathon portal origin and confirmation capacity are added across schema, validation, and admin flows. ChangesBlade Admin Config and Legacy Redirects
BloomKnights Portal Application
Participant tRPC Router and Contract
Service-specific Environment Modules
Architecture and Permissions Documentation
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Hacker
participant BloomKnightsPortal
participant bloomAuth
participant participantRouter
participant Database
Hacker->>BloomKnightsPortal: visit /apply
BloomKnightsPortal->>bloomAuth: auth()
alt no session
bloomAuth-->>Hacker: signIn Discord redirect
else session valid
BloomKnightsPortal->>participantRouter: getHackathon("bloomknights")
participantRouter->>Database: query hackathon
Database-->>participantRouter: hackathon data
Hacker->>BloomKnightsPortal: submit application
BloomKnightsPortal->>participantRouter: submitApplication
participantRouter->>Database: insert hacker record
BloomKnightsPortal-->>Hacker: redirect to /dashboard
end
sequenceDiagram
participant BladeUser
participant BladePage
participant buildParticipantPortalUrl
participant PortalUnavailable
BladeUser->>BladePage: GET legacy hacker application route
BladePage->>buildParticipantPortalUrl: portalBaseUrl + "/apply"
alt portal configured
buildParticipantPortalUrl-->>BladePage: portal URL
BladePage-->>BladeUser: redirect to BloomKnights /apply
else no portalBaseUrl
BladePage->>PortalUnavailable: render fallback
PortalUnavailable-->>BladeUser: no portal message
end
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 7 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (7 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx (1)
93-126: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNormalize
dobandgradDatebeforeform.reset()
<input type="date">only rendersYYYY-MM-DD, but this reset path passeshacker.dob/hacker.gradDatethrough unchanged. If those values areDateobjects or ISO timestamps, the edit form can show blank date fields. Match the application flow and format them first:const toDateInputValue = (value: Date | string) => { const date = value instanceof Date ? value : new Date(value); return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10); }; dob: toDateInputValue(hacker.dob), gradDate: toDateInputValue(hacker.gradDate),🤖 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/bloomknights/src/app/`(portal)/_components/hacker-profile-form.tsx around lines 93 - 126, Normalize the hacker profile date fields before calling form.reset in hacker-profile-form.tsx: the useEffect that resets the form currently passes hacker.dob and hacker.gradDate through unchanged, which can break <input type="date"> display when they are Date objects or ISO strings. Add a small formatter helper near the HackerProfileForm reset logic and use it for dob and gradDate so the values are always YYYY-MM-DD before resetting the form.
🧹 Nitpick comments (15)
apps/blade/src/app/_components/settings/sidebar-nav.tsx (1)
27-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLoading state lacks screen-reader announcement.
The error branch below uses
aria-live="polite"(Line 37) but the loading spinner doesn't announce itself to assistive tech.♿ Suggested fix
if (memberLoading) { return ( - <div className="flex h-full w-full items-center justify-center"> + <div + className="flex h-full w-full items-center justify-center" + role="status" + aria-live="polite" + > <Loader2 className="h-6 w-6 animate-spin" /> + <span className="sr-only">Loading navigation…</span> </div> ); }As per path instructions, apps/blade/** requires checking "Accessibility (alt text, ARIA, semantic HTML)".
🤖 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/blade/src/app/_components/settings/sidebar-nav.tsx` around lines 27 - 33, The loading branch in sidebar-nav.tsx does not announce its state to assistive tech, unlike the error state that already uses aria-live. Update the memberLoading return block in the SidebarNav component to add appropriate ARIA live/status semantics (for example on the wrapper around the Loader2 spinner) so screen readers are notified that loading is in progress.packages/auth/src/factory.ts (1)
54-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueHardcoded
@blade.orgemail domain in a now-shared factory.
createForgeAuthis reused by BloomKnights too, so every synthesized user email lands on@blade.orgregardless of the mounting app. Since these synthetic emails may be used downstream as identifiers, consider parameterizing the domain viaForgeAuthOptions(uniqueness is already preserved byprofile.id).export interface ForgeAuthOptions { baseURL: string; + syntheticEmailDomain?: string; }🤖 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/auth/src/factory.ts` around lines 54 - 61, The synthesized email in createForgeAuth is hardcoded to `@blade.org`, which breaks reuse in other apps like BloomKnights. Update ForgeAuthOptions to accept an email domain (or equivalent configurable value) and use that inside mapProfileToUser instead of the literal domain, while keeping the identifier based on profile.id so uniqueness remains unchanged.packages/validators/src/hackathons.ts (2)
26-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeprecated
.url()string method — considerz.url().Zod v4 deprecated the chainable
z.string().url()in favor of the top-levelz.url(); the method form still works today but will be removed in a future major version.♻️ Migration example
export const hackathonPortalOriginSchema = z - .string() + .url("Enter a valid portal URL.") .trim() - .url("Enter a valid portal URL.") .superRefine((value, 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 `@packages/validators/src/hackathons.ts` around lines 26 - 53, The hackathon portal schema uses the deprecated chainable string URL validator in hackathonPortalOriginSchema. Update the schema to use the top-level z.url() validator instead of z.string().url(), while keeping the existing trim, superRefine, and transform behavior intact so the validation logic and output origin normalization remain the same.
61-69: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueLGTM, minor edge case: whitespace-only capacity input coerces to
0.
z.coerce.number()on a whitespace-only string (e.g." ") yields0rather thanNaN/null, sinceNumber(" ") === 0. This produces the "must be greater than zero" error instead of treating it as blank/unlimited. Low risk in practice since the UI usestype="number", which normalizes such input to""before submission.🤖 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/validators/src/hackathons.ts` around lines 61 - 69, The whitespace-only input edge case in hackathonConfirmationCapacitySchema should be treated as blank instead of coercing to 0. Update the preprocess step in hackathonConfirmationCapacitySchema to normalize strings containing only whitespace to null before z.coerce.number() runs, alongside the existing empty/undefined/null handling. Use the hackathonConfirmationCapacitySchema symbol to locate the validator and keep the rest of the number validation unchanged.apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx (1)
132-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOnly the first validation issue per field is surfaced.
portalResult.error.issues[0]?.messagedrops any additional issues (e.g.,hackathonPortalOriginSchema.superRefinecan add both a protocol issue and a path/query/hash issue for the same invalid input). Minor UX nit — not blocking since the first message is still actionable.🤖 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/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx` around lines 132 - 146, Only the first validation message is being forwarded for each field in hackathon-manager.tsx, so multi-issue validations from portalResult and capacityResult lose useful feedback. Update the portalBaseUrl and confirmationCapacity handling in the validation block to collect and surface all messages from each result’s issues array (for example by combining them into one string or adding separate issues), instead of reading only error.issues[0].message. Use the existing portalResult, capacityResult, and ctx.addIssue locations to keep the fix local.apps/blade/src/lib/hackathon-portal.ts (1)
1-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate origin-validation logic vs.
packages/validators/src/hackathons.ts.
hackathonPortalOriginSchemaalready encodes the exact same "bare http(s) origin, no credentials/path/search/hash" rule used to validateportalBaseUrlon write. Re-implementing it here for the read/redirect path means the two checks can silently drift apart if one is updated and the other isn't.Consider validating with the shared schema and just handling the parse failure:
♻️ Proposed refactor to reuse the shared validator
+import { hackathonPortalOriginSchema } from "`@forge/validators/hackathons`"; + export type ParticipantPortalPath = | "/apply" | "/dashboard" | "/dashboard/profile"; export function buildParticipantPortalUrl( portalBaseUrl: string | null | undefined, path: ParticipantPortalPath, ) { if (!portalBaseUrl) return null; - try { - const baseUrl = new URL(portalBaseUrl); - if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") { - return null; - } - if ( - baseUrl.username || - baseUrl.password || - baseUrl.pathname !== "/" || - baseUrl.search || - baseUrl.hash - ) { - return null; - } - return new URL(path, baseUrl).toString(); - } catch { - return null; - } + const result = hackathonPortalOriginSchema.safeParse(portalBaseUrl); + if (!result.success) return null; + return new URL(path, result.data).toString(); }(Adjust the import path/export name to whatever
packages/validatorsactually exposes.)🤖 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/blade/src/lib/hackathon-portal.ts` around lines 1 - 30, The URL validation in buildParticipantPortalUrl duplicates the same bare-origin rule already enforced by hackathonPortalOriginSchema, so refactor this helper to reuse the shared validator instead of re-checking protocol, username/password, pathname, search, and hash locally. Update buildParticipantPortalUrl in hackathon-portal.ts to parse/validate portalBaseUrl through the shared schema from packages/validators/src/hackathons.ts (or its exported equivalent), then return null on parse failure and continue building the path URL only for valid origins.apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx (1)
13-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider centralizing the BloomKnights color palette.
Hex literals like
#FFFDF1,#42602A,#53634A,#f384d4are hardcoded here and repeat verbatim acrossapply/page.tsxanddashboard/profile/page.tsx. Pulling these into a Tailwind theme (@themein v4) or shared constants would avoid drift if the brand palette changes later.🤖 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/bloomknights/src/app/`(portal)/_components/auth-retry.tsx around lines 13 - 26, The auth retry UI is using repeated hardcoded BloomKnights hex colors, which should be centralized to avoid palette drift. Update the `AuthRetry` component to reference shared brand color tokens instead of inline literals, and reuse the same source used by `apply/page.tsx` and `dashboard/profile/page.tsx` so the palette stays consistent. Prefer a Tailwind v4 `@theme` definition or a shared constants module, then replace the existing color usages in `AuthRetry` with those shared values.apps/bloomknights/src/app/(portal)/_components/bloomknights-ambient-background.tsx (1)
7-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider consolidating inline
<style>blocks used across portal components.This component (and
bloomknights-dashboard-shell.tsx, per the referenced snippet) each inject their own raw<style>tag with keyframes/pseudo-elements. Since Tailwind v4 supports CSS-first configuration (@theme,@utility), moving these decorative animations into a shared global stylesheet would reduce duplication and make it easier to reason about the full animation set for this portal.🤖 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/bloomknights/src/app/`(portal)/_components/bloomknights-ambient-background.tsx around lines 7 - 97, The portal components are duplicating raw inline animation CSS in separate `<style>` tags, which makes the decorative styles harder to maintain. Move the keyframes, pseudo-element rules, and reduced-motion handling from `BloomknightsAmbientBackground` and the related portal shell component into a shared global stylesheet or Tailwind v4 CSS-first definitions (`@theme`/`@utility`), then keep the components only responsible for applying the class names.apps/bloomknights/src/app/(portal)/_components/portal-header.tsx (1)
21-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSign-out failures fail silently.
void signOut(...)swallows any rejection with no feedback, unlike the toast-based error handling used consistently elsewhere in the portal (e.g.,bloom-dashboard.tsx's confirm/withdraw/report-issue handlers). If sign-out fails, the user sees nothing and may believe they're logged out.♻️ Proposed fix
<Button type="button" variant="outline" className="rounded-full bg-white/85 text-[`#42602A`]" - onClick={() => void signOut({ redirectTo: "/" })} + onClick={() => { + void signOut({ redirectTo: "/" }).catch(() => { + toast.error("Could not sign out. Please try again."); + }); + }} >🤖 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/bloomknights/src/app/`(portal)/_components/portal-header.tsx around lines 21 - 28, The sign-out action in portal-header’s Button click handler swallows failures by using a fire-and-forget signOut call with no user feedback. Update the onClick handler around signOut to handle rejections explicitly, and surface a toast-based error message consistent with the portal’s other handlers in bloom-dashboard.tsx. Keep the existing Button and signOut usage, but wrap the async sign-out flow so any failure is caught and reported to the user.apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx (1)
220-233: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win"Confirmation closed" conflates two distinct reasons.
When
atCapacityis true (capacity reached) vsconfirmationClosed(deadline passed), the button shows the same "Confirmation closed" label. These are different situations for the user — a capacity cap is arguably actionable/explainable ("spots filled") while a deadline is not. Consider distinguishing the copy so waitlisted-by-capacity hackers understand why they can't confirm.- {confirmationClosed || atCapacity - ? "Confirmation closed" - : "Agree and confirm"} + {confirmationClosed + ? "Confirmation closed" + : atCapacity + ? "Confirmation full" + : "Agree and confirm"}🤖 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/bloomknights/src/app/`(portal)/_components/bloom-dashboard.tsx around lines 220 - 233, The CTA label in bloom-dashboard’s Button currently uses the same “Confirmation closed” copy for both confirmationClosed and atCapacity, which hides the real reason. Update the conditional text in the Button render so it distinguishes the two states separately, using the existing confirmationClosed and atCapacity flags in bloom-dashboard’s confirm action area. Keep the disabled behavior as-is, but change the displayed copy to reflect whether the deadline passed or capacity was reached.packages/hackathon/src/application-schema.ts (1)
71-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the Zod 4 email factory with
pipehere
z.email()is the Zod 4 replacement, but keep the required check first so the empty-string message stays intact:email: z.string().min(1, "Required").pipe(z.email({ error: "Invalid email" })),Same change at line 159.
🤖 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/hackathon/src/application-schema.ts` at line 71, The email validation in application-schema should use the Zod 4 email factory with pipe so the required-message behavior stays intact. Update the email field in the schema definitions (including the one referenced by the ApplicationSchema and the matching occurrence elsewhere) to keep the min(1, "Required") check first, then pipe into z.email({ error: "Invalid email" }) instead of chaining z.string().email(...).packages/api/src/routers/participant-portal.ts (1)
530-546: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDouble
as unknown ascast bypasses the router's structural type checking.
createTRPCRouter(...) as unknown as TRPCBuiltRouter<...>hides any mismatch between the implementation object andParticipantPortalRouterRecord. This is likely needed to give the exported const an explicit declared type (a common tRPC pattern), but per guidelines broad casts should be justified — a short comment explaining why the cast is required would help future maintainers trust it's intentional rather than a type-safety escape hatch.The same pattern repeats in
packages/api/src/participant.ts(lines 22-24).As per coding guidelines,
**/*.{ts,tsx,js,jsx}: "Do not bypass type or lint errors with any, broad casts, eslint-disable, or ignored promises unless justified".🤖 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/api/src/routers/participant-portal.ts` around lines 530 - 546, The exported routers use a double `as unknown as` cast that bypasses structural type checking and needs an explicit justification. In `participantPortalRouter` (and the matching export in `participant.ts`), add a short inline comment explaining why the cast to `TRPCBuiltRouter<...>` is required, so the intent is clear and future readers know it is a deliberate tRPC typing pattern rather than an unsafe escape hatch.Source: Coding guidelines
packages/api/src/storage-env.ts (1)
1-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
no-restricted-propertiesdisable doesn't match the "env.ts/.config." exclusion pattern.This file directly reads
process.envviacreateEnv, gated with/* eslint-disable no-restricted-properties */. As per coding guidelines,!(**/{env,*.config}.{js,ts,tsx}): "Flag any direct usage of process.env outside of env.ts config files and .config. files. The codebase uses validated env imports".storage-env.ts(and its siblingsdiscord-env.ts,google-env.ts,stripe-env.tsfrom the same cohort) don't match either glob literal (env.tsor*.config.*), so per a strict reading, the lint rule's exception list should be updated to also cover this new*-env.tsnaming convention rather than relying on a local eslint-disable in each new module. Also relevant per**/*.{ts,tsx,js,jsx}: "Do not bypass type or lint errors with ... eslint-disable ... unless justified" — this disable is reasonably justified (it is a validated env config module), but the underlying lint/config pattern should reflect that so future files don't need a per-file suppression.♻️ Suggested ESLint config update (outside this file)
- restrictedProperties for files matching: env.{js,ts,tsx}, *.config.{js,ts,tsx} + restrictedProperties exception glob: **/{env,*-env,*.config}.{js,ts,tsx}🤖 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/api/src/storage-env.ts` around lines 1 - 14, The issue is that the validated env module uses a local no-restricted-properties disable because the lint exception pattern only covers env.ts and *.config.* files. Update the ESLint rule/config exception to include the new *-env.ts naming convention used by storageEnv and its sibling env modules (discordEnv, googleEnv, stripeEnv), so these files can use process.env through createEnv without per-file suppressions. Keep the existing storageEnv implementation unchanged and adjust the shared lint rule instead of adding more eslint-disable comments.Source: Coding guidelines
packages/utils/src/discord-env.ts (2)
4-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated
skipValidationboilerplate into a shared helper.The same
createEnv({ ..., skipValidation: !!process.env.CI || process.env.npm_lifecycle_event === "lint" })pattern is duplicated verbatim ingoogle-env.tsandstripe-env.ts(and likelystorage-env.ts). A shared helper keeps the CI/lint bypass logic in one place and reduces drift risk if the condition ever needs to change.Also consider
.min(1)on the token schema so an accidentally empty string doesn't silently pass validation.♻️ Proposed shared helper
+// packages/utils/src/create-service-env.ts +import { createEnv } from "`@t3-oss/env-core`"; + +export const skipServiceEnvValidation = + !!process.env.CI || process.env.npm_lifecycle_event === "lint";import { createEnv } from "`@t3-oss/env-core`"; import { z } from "zod"; +import { skipServiceEnvValidation } from "./create-service-env"; export const discordEnv = createEnv({ - server: { DISCORD_BOT_TOKEN: z.string() }, + server: { DISCORD_BOT_TOKEN: z.string().min(1) }, runtimeEnv: process.env, - skipValidation: - !!process.env.CI || process.env.npm_lifecycle_event === "lint", + skipValidation: skipServiceEnvValidation, });🤖 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/utils/src/discord-env.ts` around lines 4 - 9, The `createEnv` setup in `discordEnv` repeats the same `skipValidation` CI/lint condition used in other env modules, so move that logic into a shared helper and reuse it from `discordEnv` (and the matching `googleEnv`/`stripeEnv`/`storageEnv` callers) to keep behavior centralized. Also tighten the `DISCORD_BOT_TOKEN` schema in `createEnv` by using the token string validator with a non-empty constraint so an empty value cannot pass validation.
4-9: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsolidate the Discord token env schema
packages/utils/src/env.tsstill declaresDISCORD_BOT_TOKEN, sopackages/utils/src/env.tsandpackages/utils/src/discord-env.tsboth validate the same secret. Remove the duplicate or havepackages/utils/src/discord.tsreuse the shared schema to keep boot-time validation from drifting.🤖 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/utils/src/discord-env.ts` around lines 4 - 9, The Discord bot token is being validated twice, once in discordEnv and again in the shared env schema, which can drift over time. Update packages/utils/src/discord-env.ts and packages/utils/src/env.ts so DISCORD_BOT_TOKEN is declared in only one place, then have packages/utils/src/discord.ts consume that shared source instead of defining its own schema. Keep the runtime validation behavior the same while consolidating the schema around the existing createEnv setup.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@apps/bloomknights/src/app/_components/navbar/Navbar.tsx`:
- Around line 30-41: The signed-out auth links in Navbar use the same href for
both “Sign up” and “Log in”, which will collide with NavContent’s
key={link.href} rendering. Update the authLinks entries in Navbar so each item
has a unique href or otherwise provides distinct key material, and keep the
existing NavContent map keyed by link.href working without duplicate React keys.
In `@apps/bloomknights/src/app/`(portal)/_components/bloom-dashboard.tsx:
- Around line 271-306: The QR dialog flow in bloom-dashboard’s
Dialog/DialogContent path only handles qrCode vs loading, so a failed
loadQRCode() leaves the Loader2 spinner visible forever. Update the QR modal
state handling around qrMutation in bloom-dashboard to render an explicit error
state when qrMutation.isError is true, and provide a retry action that calls
loadQRCode() again; make sure the existing qrCode and loading branches still
work correctly.
In `@apps/bloomknights/src/app/api/trpc/`[trpc]/route.ts:
- Around line 9-16: The current MAX_REQUEST_SIZE check in handler only trusts
the Content-Length header, so requests without it or with a spoofed value can
bypass the limit. Update the route-level enforcement in handler to validate the
actual request body size before passing the request into the tRPC handler,
and/or add a real server-side body size cap in next.config.js so oversized
uploads are rejected even when Content-Length is missing or incorrect. Use the
existing handler and MAX_REQUEST_SIZE symbols to locate the fix.
In `@packages/api/src/routers/participant-portal.ts`:
- Around line 408-439: `withdrawAttendance` has a TOCTOU race because it checks
`participant.status` in a અલગ read but the `db.update(HackerAttendee)` call does
not re-assert that status in its `where` clause. Update the `withdrawAttendance`
mutation to make the write conditional on the current status (using the same
`HackerAttendee` row keyed by `hackerId` and `hackathonId`, plus a `status =
"confirmed"` predicate, or equivalent transactional locking/re-check) so a
concurrent change cannot still withdraw a non-confirmed attendee.
- Around line 88-90: The getHackathon query in participant-portal is returning
the full SelectHackathon record instead of a public-safe DTO. Update the
publicProcedure resolver to map the requireHackathon result to an explicit
public projection, excluding internal fields like applicationBackgroundKey,
emailTemplateKey, portalBaseUrl, and confirmationCapacity. Then adjust
ParticipantPortalContract.getHackathon to use the same narrowed shape so the API
contract matches the sanitized response.
In `@packages/api/src/trpc.ts`:
- Around line 41-42: Remove the unconditional debug logging in tRPC request
handling: the `console.log` in `createCaller`/request setup that prints `source`
and `session?.user` should not run in production. Delete the log entirely, or if
request tracing is still needed, replace it with a redacted log that does not
include the full user object or any PII and is gated behind an explicit debug
flag.
In `@packages/auth/src/factory.ts`:
- Line 9: Update the discord import in the auth factory to use the package
entrypoint from `@forge/utils` instead of reaching into ../../utils/src/discord,
and add `@forge/utils` as an explicit dependency in packages/auth/package.json.
Use the existing factory.ts import site and the discord symbol to locate the
change, and make sure the package boundary is respected everywhere this import
is used.
In `@packages/hackathon/src/application-schema.ts`:
- Around line 213-214: The two agreement refinements in application-schema’s
profile schema currently use z.boolean().refine(...) without user-facing
messages, so they fall back to a generic validation error. Update the
refinements on agreesToMLHCodeOfConduct and agreesToMLHDataSharing to include
explicit message text, matching the style used elsewhere in applicationSchema so
users see a clear explanation when either checkbox is unchecked.
In `@packages/hackathon/src/server.ts`:
- Around line 7-15: The createHackathonPortalServerCaller helper is passing a
thunk into createParticipantCaller instead of the resolved TRPC context, which
causes participantRouter.createCaller to receive a function rather than the
context object. Update createHackathonPortalServerCaller to construct the
context with createTRPCContext and pass that context value directly through
createParticipantCaller so participant procedures can access ctx.session and
ctx.token correctly.
In `@packages/validators/src/hacker.ts`:
- Around line 32-33: The Hacker schema currently treats agreesToMLHCodeOfConduct
and agreesToMLHDataSharing as plain booleans, so false is accepted as valid
input. Update the validation in the hacker schema to require explicit acceptance
for both fields, using the existing Zod schema for this object so that only true
passes and false is rejected.
---
Outside diff comments:
In `@apps/bloomknights/src/app/`(portal)/_components/hacker-profile-form.tsx:
- Around line 93-126: Normalize the hacker profile date fields before calling
form.reset in hacker-profile-form.tsx: the useEffect that resets the form
currently passes hacker.dob and hacker.gradDate through unchanged, which can
break <input type="date"> display when they are Date objects or ISO strings. Add
a small formatter helper near the HackerProfileForm reset logic and use it for
dob and gradDate so the values are always YYYY-MM-DD before resetting the form.
---
Nitpick comments:
In `@apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx`:
- Around line 132-146: Only the first validation message is being forwarded for
each field in hackathon-manager.tsx, so multi-issue validations from
portalResult and capacityResult lose useful feedback. Update the portalBaseUrl
and confirmationCapacity handling in the validation block to collect and surface
all messages from each result’s issues array (for example by combining them into
one string or adding separate issues), instead of reading only
error.issues[0].message. Use the existing portalResult, capacityResult, and
ctx.addIssue locations to keep the fix local.
In `@apps/blade/src/app/_components/settings/sidebar-nav.tsx`:
- Around line 27-33: The loading branch in sidebar-nav.tsx does not announce its
state to assistive tech, unlike the error state that already uses aria-live.
Update the memberLoading return block in the SidebarNav component to add
appropriate ARIA live/status semantics (for example on the wrapper around the
Loader2 spinner) so screen readers are notified that loading is in progress.
In `@apps/blade/src/lib/hackathon-portal.ts`:
- Around line 1-30: The URL validation in buildParticipantPortalUrl duplicates
the same bare-origin rule already enforced by hackathonPortalOriginSchema, so
refactor this helper to reuse the shared validator instead of re-checking
protocol, username/password, pathname, search, and hash locally. Update
buildParticipantPortalUrl in hackathon-portal.ts to parse/validate portalBaseUrl
through the shared schema from packages/validators/src/hackathons.ts (or its
exported equivalent), then return null on parse failure and continue building
the path URL only for valid origins.
In `@apps/bloomknights/src/app/`(portal)/_components/auth-retry.tsx:
- Around line 13-26: The auth retry UI is using repeated hardcoded BloomKnights
hex colors, which should be centralized to avoid palette drift. Update the
`AuthRetry` component to reference shared brand color tokens instead of inline
literals, and reuse the same source used by `apply/page.tsx` and
`dashboard/profile/page.tsx` so the palette stays consistent. Prefer a Tailwind
v4 `@theme` definition or a shared constants module, then replace the existing
color usages in `AuthRetry` with those shared values.
In `@apps/bloomknights/src/app/`(portal)/_components/bloom-dashboard.tsx:
- Around line 220-233: The CTA label in bloom-dashboard’s Button currently uses
the same “Confirmation closed” copy for both confirmationClosed and atCapacity,
which hides the real reason. Update the conditional text in the Button render so
it distinguishes the two states separately, using the existing
confirmationClosed and atCapacity flags in bloom-dashboard’s confirm action
area. Keep the disabled behavior as-is, but change the displayed copy to reflect
whether the deadline passed or capacity was reached.
In
`@apps/bloomknights/src/app/`(portal)/_components/bloomknights-ambient-background.tsx:
- Around line 7-97: The portal components are duplicating raw inline animation
CSS in separate `<style>` tags, which makes the decorative styles harder to
maintain. Move the keyframes, pseudo-element rules, and reduced-motion handling
from `BloomknightsAmbientBackground` and the related portal shell component into
a shared global stylesheet or Tailwind v4 CSS-first definitions
(`@theme`/`@utility`), then keep the components only responsible for applying
the class names.
In `@apps/bloomknights/src/app/`(portal)/_components/portal-header.tsx:
- Around line 21-28: The sign-out action in portal-header’s Button click handler
swallows failures by using a fire-and-forget signOut call with no user feedback.
Update the onClick handler around signOut to handle rejections explicitly, and
surface a toast-based error message consistent with the portal’s other handlers
in bloom-dashboard.tsx. Keep the existing Button and signOut usage, but wrap the
async sign-out flow so any failure is caught and reported to the user.
In `@packages/api/src/routers/participant-portal.ts`:
- Around line 530-546: The exported routers use a double `as unknown as` cast
that bypasses structural type checking and needs an explicit justification. In
`participantPortalRouter` (and the matching export in `participant.ts`), add a
short inline comment explaining why the cast to `TRPCBuiltRouter<...>` is
required, so the intent is clear and future readers know it is a deliberate tRPC
typing pattern rather than an unsafe escape hatch.
In `@packages/api/src/storage-env.ts`:
- Around line 1-14: The issue is that the validated env module uses a local
no-restricted-properties disable because the lint exception pattern only covers
env.ts and *.config.* files. Update the ESLint rule/config exception to include
the new *-env.ts naming convention used by storageEnv and its sibling env
modules (discordEnv, googleEnv, stripeEnv), so these files can use process.env
through createEnv without per-file suppressions. Keep the existing storageEnv
implementation unchanged and adjust the shared lint rule instead of adding more
eslint-disable comments.
In `@packages/auth/src/factory.ts`:
- Around line 54-61: The synthesized email in createForgeAuth is hardcoded to
`@blade.org`, which breaks reuse in other apps like BloomKnights. Update
ForgeAuthOptions to accept an email domain (or equivalent configurable value)
and use that inside mapProfileToUser instead of the literal domain, while
keeping the identifier based on profile.id so uniqueness remains unchanged.
In `@packages/hackathon/src/application-schema.ts`:
- Line 71: The email validation in application-schema should use the Zod 4 email
factory with pipe so the required-message behavior stays intact. Update the
email field in the schema definitions (including the one referenced by the
ApplicationSchema and the matching occurrence elsewhere) to keep the min(1,
"Required") check first, then pipe into z.email({ error: "Invalid email" })
instead of chaining z.string().email(...).
In `@packages/utils/src/discord-env.ts`:
- Around line 4-9: The `createEnv` setup in `discordEnv` repeats the same
`skipValidation` CI/lint condition used in other env modules, so move that logic
into a shared helper and reuse it from `discordEnv` (and the matching
`googleEnv`/`stripeEnv`/`storageEnv` callers) to keep behavior centralized. Also
tighten the `DISCORD_BOT_TOKEN` schema in `createEnv` by using the token string
validator with a non-empty constraint so an empty value cannot pass validation.
- Around line 4-9: The Discord bot token is being validated twice, once in
discordEnv and again in the shared env schema, which can drift over time. Update
packages/utils/src/discord-env.ts and packages/utils/src/env.ts so
DISCORD_BOT_TOKEN is declared in only one place, then have
packages/utils/src/discord.ts consume that shared source instead of defining its
own schema. Keep the runtime validation behavior the same while consolidating
the schema around the existing createEnv setup.
In `@packages/validators/src/hackathons.ts`:
- Around line 26-53: The hackathon portal schema uses the deprecated chainable
string URL validator in hackathonPortalOriginSchema. Update the schema to use
the top-level z.url() validator instead of z.string().url(), while keeping the
existing trim, superRefine, and transform behavior intact so the validation
logic and output origin normalization remain the same.
- Around line 61-69: The whitespace-only input edge case in
hackathonConfirmationCapacitySchema should be treated as blank instead of
coercing to 0. Update the preprocess step in hackathonConfirmationCapacitySchema
to normalize strings containing only whitespace to null before z.coerce.number()
runs, alongside the existing empty/undefined/null handling. Use the
hackathonConfirmationCapacitySchema symbol to locate the validator and keep the
rest of the number validation unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 6aa5b2f3-be81-4c10-a4f7-45a8b8b49903
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (114)
.env.exampleapps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsxapps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsxapps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsxapps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsxapps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsxapps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsxapps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsxapps/blade/src/app/_components/dashboard/hacker-dashboard/confirm-button.tsxapps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsxapps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsxapps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-qr-button.tsxapps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-resume-button.tsxapps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsxapps/blade/src/app/_components/dashboard/hacker/hackbackgrounds/khix.tsapps/blade/src/app/_components/hackathon/portal-unavailable.tsxapps/blade/src/app/_components/option-cards.tsxapps/blade/src/app/_components/providers.tsxapps/blade/src/app/_components/settings/delete-hacker-button.tsxapps/blade/src/app/_components/settings/sidebar-nav.tsxapps/blade/src/app/_components/theme-toggle-route-guard.tsxapps/blade/src/app/dashboard/page.tsxapps/blade/src/app/hackathon/README.mdapps/blade/src/app/hackathon/[slug]/page.tsxapps/blade/src/app/hackathon/bloomknights/page.tsxapps/blade/src/app/hackathon/page.tsxapps/blade/src/app/hacker/application/[hackathon-id]/page.tsxapps/blade/src/app/settings/hacker-profile/page.tsxapps/blade/src/app/settings/page.tsxapps/blade/src/consts/index.tsapps/blade/src/lib/hackathon-portal.tsapps/bloomknights/next.config.jsapps/bloomknights/package.jsonapps/bloomknights/src/app/(marketing)/layout.tsxapps/bloomknights/src/app/(marketing)/page.tsxapps/bloomknights/src/app/(portal)/_components/application/hackbackgrounds/bloomknights.tsapps/bloomknights/src/app/(portal)/_components/application/hackbackgrounds/index.tsapps/bloomknights/src/app/(portal)/_components/application/hackbackgrounds/types.tsapps/bloomknights/src/app/(portal)/_components/application/hacker-application-background.tsxapps/bloomknights/src/app/(portal)/_components/application/hacker-application-form.tsxapps/bloomknights/src/app/(portal)/_components/auth-retry.tsxapps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-action-blooms.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-ambient-background.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-shell.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-flower-cursor.tsxapps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsxapps/bloomknights/src/app/(portal)/_components/portal-header.tsxapps/bloomknights/src/app/(portal)/apply/page.tsxapps/bloomknights/src/app/(portal)/dashboard/page.tsxapps/bloomknights/src/app/(portal)/dashboard/profile/page.tsxapps/bloomknights/src/app/(portal)/hacker/application/bloomknights/page.tsxapps/bloomknights/src/app/(portal)/layout.tsxapps/bloomknights/src/app/_components/navbar/NavContent.tsxapps/bloomknights/src/app/_components/navbar/Navbar.tsxapps/bloomknights/src/app/_components/register/registerButton.tsxapps/bloomknights/src/app/api/auth/[...all]/route.tsapps/bloomknights/src/app/api/auth/signin/route.tsapps/bloomknights/src/app/api/trpc/[trpc]/route.tsapps/bloomknights/src/app/layout.tsxapps/bloomknights/src/auth/client.tsapps/bloomknights/src/auth/server.tsapps/bloomknights/src/env.tsapps/bloomknights/src/lib/portal-config.tsdocs/API-AND-PERMISSIONS.mddocs/ARCHITECTURE.mdpackages/api/package.jsonpackages/api/src/minio/minio-client.tspackages/api/src/participant-contract.tspackages/api/src/participant.tspackages/api/src/resume-storage.tspackages/api/src/routers/hackathon.tspackages/api/src/routers/hackers/mutations.tspackages/api/src/routers/participant-portal.tspackages/api/src/storage-env.tspackages/api/src/trpc.tspackages/auth/package.jsonpackages/auth/src/callback-url.tspackages/auth/src/client-factory.tspackages/auth/src/config.tspackages/auth/src/env.tspackages/auth/src/factory.tspackages/auth/src/index.rsc.tspackages/auth/src/index.tspackages/auth/src/server-factory.tspackages/auth/src/shared-env.tspackages/db/drizzle/0010_wooden_supreme_intelligence.sqlpackages/db/drizzle/meta/0010_snapshot.jsonpackages/db/drizzle/meta/_journal.jsonpackages/db/src/schemas/knight-hacks.tspackages/hackathon/README.mdpackages/hackathon/eslint.config.jspackages/hackathon/package.jsonpackages/hackathon/src/application-schema.tspackages/hackathon/src/client.tsxpackages/hackathon/src/config.tspackages/hackathon/src/index.tspackages/hackathon/src/lifecycle.tspackages/hackathon/src/query-client.tspackages/hackathon/src/server.tspackages/hackathon/src/types.tspackages/hackathon/tsconfig.jsonpackages/utils/src/discord-env.tspackages/utils/src/discord.tspackages/utils/src/google-env.tspackages/utils/src/google.tspackages/utils/src/stripe-env.tspackages/utils/src/stripe.tspackages/validators/package.jsonpackages/validators/src/hackathons.tspackages/validators/src/hacker.tspackages/validators/src/index.tsturbo.json
💤 Files with no reviewable changes (19)
- apps/blade/src/app/_components/dashboard/hacker-dashboard/confirm-button.tsx
- apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-qr-button.tsx
- apps/blade/src/consts/index.ts
- apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx
- apps/blade/src/app/_components/theme-toggle-route-guard.tsx
- apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx
- apps/blade/src/app/_components/dashboard/hacker/hackbackgrounds/khix.ts
- apps/blade/src/app/settings/page.tsx
- apps/blade/src/app/_components/settings/delete-hacker-button.tsx
- apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx
- apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx
- apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx
- apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx
- apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx
- apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx
- apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-resume-button.tsx
- apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx
- apps/blade/src/app/hackathon/bloomknights/page.tsx
- packages/auth/src/config.ts
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/blade/src/env.client.ts (1)
8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate schema for
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.This key is now validated here AND still in
apps/blade/src/env.ts'sclientsection (with matchingruntimeEnvmapping). Sincecheckout-form.tsxwas updated to consumeclientEnvinstead ofenv, the copy inenv.tslooks like leftover duplication — two sources of truth for the same variable increase drift risk if one gets updated without the other.If nothing else still reads
env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, remove it fromapps/blade/src/env.ts.🤖 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/blade/src/env.client.ts` at line 8, The Stripe publishable key is being validated in two places, creating duplicate sources of truth between env.client and env.ts. Remove NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY from the client schema in env.ts if nothing else still reads env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, and keep the single validation in env.client so checkout-form.tsx continues using clientEnv consistently.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@apps/blade/src/app/auth/bloom-return/route.ts`:
- Around line 6-16: The origin allowlist in getBloomReturnURL currently always
includes the localhost origin via ALLOWED_BLOOM_RETURN_ORIGINS, which creates an
open-redirect path in production. Update the allowlist construction so the
localhost entry is only added in local/development environments, while keeping
configuredBloomOrigin and the production Bloom origin allowed as-is. Use the
existing getBloomReturnURL and ALLOWED_BLOOM_RETURN_ORIGINS symbols to locate
the logic and gate the localhost origin behind an environment check before
building allowedOrigins.
In `@apps/blade/src/env.client.ts`:
- Line 7: Update the env schema in env.client.ts so NEXT_PUBLIC_BLADE_URL is not
silently defaulted to localhost in production; instead, make it required when
running in production and keep the localhost fallback only for local
development. Adjust the zod definition around NEXT_PUBLIC_BLADE_URL and any
related env validation helper so missing deployment configuration fails fast
before getBaseUrl() and tRPC self-fetch are used.
---
Nitpick comments:
In `@apps/blade/src/env.client.ts`:
- Line 8: The Stripe publishable key is being validated in two places, creating
duplicate sources of truth between env.client and env.ts. Remove
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY from the client schema in env.ts if nothing
else still reads env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, and keep the single
validation in env.client so checkout-form.tsx continues using clientEnv
consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: cedbe889-5b7f-4165-a502-953c3bad2da0
⛔ Files ignored due to path filters (1)
apps/bloomknights/public/knighthacks.svgis excluded by!**/*.svg
📒 Files selected for processing (30)
.env.exampleapps/blade/src/app/_components/dashboard/member/checkout-form.tsxapps/blade/src/app/auth/bloom-return/route.tsapps/blade/src/env.client.tsapps/blade/src/env.tsapps/blade/src/trpc/react.tsxapps/bloomknights/src/app/(marketing)/home-page-client.tsxapps/bloomknights/src/app/(marketing)/page.tsxapps/bloomknights/src/app/(portal)/_components/application/hacker-application-form.tsxapps/bloomknights/src/app/(portal)/_components/auth-retry.tsxapps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsxapps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-shell.tsxapps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsxapps/bloomknights/src/app/(portal)/_components/portal-header.tsxapps/bloomknights/src/app/(portal)/apply/page.tsxapps/bloomknights/src/app/(portal)/dashboard/layout.tsxapps/bloomknights/src/app/(portal)/dashboard/page.tsxapps/bloomknights/src/app/(portal)/dashboard/profile/page.tsxapps/bloomknights/src/app/_components/navbar/NavContent.tsxapps/bloomknights/src/app/_components/navbar/Navbar.tsxapps/bloomknights/src/app/_components/register/registerButton.tsxapps/bloomknights/src/app/api/auth/signin/route.tsapps/bloomknights/src/app/globals.cssapps/bloomknights/src/app/layout.tsxapps/bloomknights/src/env.tspackages/auth/package.jsonpackages/hackathon/README.mdpackages/hackathon/src/application-schema.tsturbo.json
💤 Files with no reviewable changes (1)
- apps/bloomknights/src/app/_components/navbar/Navbar.tsx
✅ Files skipped from review due to trivial changes (2)
- apps/bloomknights/src/app/(portal)/dashboard/layout.tsx
- packages/hackathon/README.md
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsx
- turbo.json
- apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx
- apps/bloomknights/src/app/layout.tsx
- apps/bloomknights/src/app/_components/navbar/NavContent.tsx
- packages/hackathon/src/application-schema.ts
- apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx
Adr1an04
left a comment
There was a problem hiding this comment.
viewed all tested and verified safety
There was a problem hiding this comment.
🧹 Nitpick comments (3)
apps/blade/src/env.ts (1)
8-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider migrating to Zod 4's top-level
z.url().
z.string().url()is deprecated in Zod 4 in favor of the top-levelz.url(); the chained form still works but will be removed in a future major version.♻️ Optional migration
const bloomKnightsUrlSchema = process.env.NODE_ENV === "production" - ? z.string().url() - : z.string().url().default("http://localhost:3006"); + ? z.url() + : z.url().default("http://localhost:3006");🤖 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/blade/src/env.ts` around lines 8 - 12, The URL schema in bloomKnightsUrlSchema uses the deprecated chained form from Zod 4. Update the schema to use the top-level z.url() while preserving the existing production and non-production behavior, and keep the default localhost value on the non-production branch. Locate and adjust the schema definition in env.ts so future Zod upgrades don’t break it.apps/bloomknights/src/app/api/trpc/[trpc]/route.ts (2)
29-55: 🧹 Nitpick | 🔵 TrivialDefense-in-depth: consider a platform/proxy-level body-size cap too.
The in-handler streaming limit is a solid application-level fix, but it doesn't stop the underlying Node/edge runtime (or a fronting proxy/CDN) from buffering the same oversized payload before your code ever runs. Consider adding a platform-level max body size (reverse proxy, WAF, or hosting-provider setting) as defense-in-depth against large uploads consuming resources upstream of this handler.
🤖 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/bloomknights/src/app/api/trpc/`[trpc]/route.ts around lines 29 - 55, The streaming guard in readRequestWithLimit is good, but oversized bodies can still be buffered before this handler runs. Keep the existing MAX_REQUEST_SIZE enforcement in route.ts, and also add a platform/proxy-level request body cap (for example at the reverse proxy, WAF, or hosting provider) so the trpc/[trpc]/route handler is protected upstream as defense-in-depth.
57-66: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valuePast content-length bypass issue is resolved.
The new
readRequestWithLimitstreaming check enforces the real 8MB cap on actual bytes read, closing the gap where a missing/spoofedcontent-lengthheader could bypass the earlier soft check. Good fix.One minor point:
validateToken()runs only after the full (up to 8MB) body has been buffered. Since it doesn't depend on the request body, calling it earlier (in parallel with, or before,readRequestWithLimit) would avoid wasting memory/CPU buffering large payloads from unauthenticated callers before rejecting them.♻️ Optional reordering
const handler = async (req: Request) => { const contentLength = Number(req.headers.get("content-length") ?? 0); if (contentLength > MAX_REQUEST_SIZE) { return requestTooLargeResponse(); } - const limitedRequest = await readRequestWithLimit(req); + const [session, limitedRequest] = await Promise.all([ + validateToken(), + readRequestWithLimit(req), + ]); if (!limitedRequest) return requestTooLargeResponse(); - const session = await validateToken(); return fetchRequestHandler({🤖 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/bloomknights/src/app/api/trpc/`[trpc]/route.ts around lines 57 - 66, Move the `validateToken()` call in the `handler` for the TRPC route to run before, or in parallel with, `readRequestWithLimit` so unauthenticated requests are rejected without first buffering up to 8MB of body data. Keep the existing `requestTooLargeResponse` behavior and the streaming size cap logic intact, but reorder the authentication check and body القراءة in `handler` to avoid unnecessary memory/CPU work.
🤖 Prompt for all review comments with 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.
Nitpick comments:
In `@apps/blade/src/env.ts`:
- Around line 8-12: The URL schema in bloomKnightsUrlSchema uses the deprecated
chained form from Zod 4. Update the schema to use the top-level z.url() while
preserving the existing production and non-production behavior, and keep the
default localhost value on the non-production branch. Locate and adjust the
schema definition in env.ts so future Zod upgrades don’t break it.
In `@apps/bloomknights/src/app/api/trpc/`[trpc]/route.ts:
- Around line 29-55: The streaming guard in readRequestWithLimit is good, but
oversized bodies can still be buffered before this handler runs. Keep the
existing MAX_REQUEST_SIZE enforcement in route.ts, and also add a
platform/proxy-level request body cap (for example at the reverse proxy, WAF, or
hosting provider) so the trpc/[trpc]/route handler is protected upstream as
defense-in-depth.
- Around line 57-66: Move the `validateToken()` call in the `handler` for the
TRPC route to run before, or in parallel with, `readRequestWithLimit` so
unauthenticated requests are rejected without first buffering up to 8MB of body
data. Keep the existing `requestTooLargeResponse` behavior and the streaming
size cap logic intact, but reorder the authentication check and body القراءة in
`handler` to avoid unnecessary memory/CPU work.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: ca30f677-b456-41aa-895e-a788bfc96770
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (15)
apps/blade/src/app/auth/bloom-return/route.tsapps/blade/src/env.client.tsapps/blade/src/env.tsapps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsxapps/bloomknights/src/app/api/trpc/[trpc]/route.tsapps/bloomknights/src/env.tspackages/api/src/participant-contract.tspackages/api/src/routers/participant-portal.tspackages/api/src/trpc.tspackages/auth/package.jsonpackages/auth/src/factory.tspackages/hackathon/src/application-schema.tspackages/utils/package.jsonpackages/utils/src/discord.tspackages/validators/src/hacker.ts
💤 Files with no reviewable changes (1)
- packages/utils/package.json
🚧 Files skipped from review as they are similar to previous changes (11)
- apps/blade/src/env.client.ts
- apps/bloomknights/src/env.ts
- packages/auth/src/factory.ts
- packages/auth/package.json
- apps/blade/src/app/auth/bloom-return/route.ts
- packages/api/src/trpc.ts
- packages/validators/src/hacker.ts
- packages/api/src/participant-contract.ts
- packages/api/src/routers/participant-portal.ts
- packages/hackathon/src/application-schema.ts
- apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx
Summary
bloom.knighthacks.org@forge/hackathontoolkit for future event portalsMadu dashboard and Bloom visual work is ported with co-author attribution. The
reforge/mainexperiment is not included.Verification
pnpm formatpnpm lintpnpm lint:wspnpm typecheck@forge/hackathonDeployment order
BLOOMKNIGHTS_URL=https://bloom.knighthacks.org.https://bloom.knighthacks.org/api/auth/callback/discordandhttp://localhost:3006/api/auth/callback/discordin Discord.0010_wooden_supreme_intelligence.Closes #485
Summary by CodeRabbit