Skip to content

feat: add redemption code for hackathon#237

Merged
zhubzy merged 5 commits intomainfrom
feat/redeption-code
Dec 22, 2025
Merged

feat: add redemption code for hackathon#237
zhubzy merged 5 commits intomainfrom
feat/redeption-code

Conversation

@zhubzy
Copy link
Contributor

@zhubzy zhubzy commented Dec 22, 2025

Summary

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have added appropriate tests for my changes
  • I have run pnpm check and all tests pass
  • I have tested my changes locally
  • I have linked relevant issues
  • I have added screenshots for UI changes (if applicable)

Screenshots (if applicable)

Additional Context

Summary by CodeRabbit

  • New Features
    • Hackathon offer banner displays active promotional offers with formatted expiration in the monthly usage UI.
    • Coupon redemption UI lets users enter codes, see loading state, success/error feedback, and guidance.
    • Settings page added and reachable via a new Sidebar Settings item to manage promotions and redeem coupons.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 22, 2025 16:07
@coderabbitai
Copy link

coderabbitai bot commented Dec 22, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a hackathon coupon redemption feature: frontend UI and settings route, a redeem-coupon hook, backend redeem endpoint and status exposure with Clerk privateMetadata, shared schemas for request/response and offer shape, env config for codes, an in-memory rate limiter, and runtime/test updates to sanitize/process.env protections.

Changes

Cohort / File(s) Change Summary
Frontend — Components
apps/bubble-studio/src/components/MonthlyUsageBar.tsx, apps/bubble-studio/src/components/RedeemCouponSection.tsx, apps/bubble-studio/src/components/Sidebar.tsx
Adds hackathon offer banner to MonthlyUsageBar; new RedeemCouponSection component with input, validation, redeem flow and success/error UI; adds Settings item to sidebar navigation.
Frontend — Pages & Routes
apps/bubble-studio/src/pages/SettingsPage.tsx, apps/bubble-studio/src/routes/settings.tsx, apps/bubble-studio/src/routeTree.gen.ts
New SettingsPage embedding RedeemCouponSection; route module with auth guard redirect; route tree updated to include /settings and related type augmentations.
Frontend — Hook
apps/bubble-studio/src/hooks/useRedeemCoupon.ts
New React Query mutation hook to POST /subscription/redeem, handle ApiHttpError payloads, invalidate subscription query on success, and expose mutation state.
Backend — API & Auth
apps/bubblelab-api/src/routes/subscription.ts, apps/bubblelab-api/src/middleware/auth.ts, apps/bubblelab-api/src/schemas/subscription.ts
New POST /subscription/redeem route with validation, normalization, 1-day expiration computation, Clerk privateMetadata update, rate limiting; subscription status now includes hackathonOffer; auth middleware applies temporary pro_plus override when active offer present; OpenAPI schema added.
Backend — Rate Limiting
apps/bubblelab-api/src/middleware/rate-limit.ts, apps/bubblelab-api/src/middleware/rate-limit.test.ts
Adds in-memory rate-limit middleware with headers, Retry-After support, periodic cleanup; exports couponRedemptionRateLimit; tests validating limits and headers.
Backend — Config
apps/bubblelab-api/.env.example, apps/bubblelab-api/src/config/env.ts
Adds HACKATHON_COUPON_CODES env var (and WISPR_API_KEY in example); exposes and logs HACKATHON_COUPON_CODES presence.
Shared Schemas
packages/bubble-shared-schemas/src/subscription-status-schema.ts
Adds HackathonOffer, RedeemCouponRequest, RedeemCouponResponse schemas/types; includes optional hackathonOffer in SubscriptionStatusResponse.
Runtime — Sanitizer & Runner
packages/bubble-runtime/src/utils/sanitize-script.ts, packages/bubble-runtime/src/runtime/BubbleRunner.ts
Changes sanitizeScript signature and implements staged protect/replace/restore flow to block real process.env access (dot/bracket/standalone) while preserving strings/comments; BubbleRunner.runAll no longer passes temp file path into sanitizer.
Runtime — Tests & Fixtures
packages/bubble-runtime/src/runtime/BubbleRunner.test.ts, packages/bubble-runtime/tests/fixtures/*, packages/bubble-runtime/tests/fixtures/index.ts
Adds fixtures for malicious and legitimate process.env usages; centralizes script validation via expectValidScript; new tests asserting sanitizer blocks unauthorized process.env access and permits string/comment mentions.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as RedeemCouponSection (Frontend)
    participant Hook as useRedeemCoupon
    participant API as BubbleLab API (/subscription/redeem)
    participant Clerk as Clerk (identity store)
    participant Status as /subscription/status

    User->>UI: Enter coupon code & submit
    UI->>Hook: mutate(code)
    Hook->>API: POST /subscription/redeem { code }
    API->>API: normalize & validate code vs HACKATHON_COUPON_CODES
    alt invalid
        API-->>Hook: 400 { success: false, message }
    else valid
        API->>API: calculate expiresAt (now + 1 day)
        API->>Clerk: update privateMetadata.hackathonOffer { code, redeemedAt, expiresAt }
        alt Clerk success
            API-->>Hook: 200 { success: true, message, expiresAt }
            Hook->>UI: mutation success (invalidate subscription)
            UI->>User: show success & offer banner
        else Clerk failure
            API-->>Hook: error response
            Hook->>UI: show error
        end
    end
    Note right of Hook: subsequent subscription status fetch includes hackathonOffer
    UI->>Status: GET /subscription/status (after invalidate)
    Status-->>UI: subscription + hackathonOffer
    UI->>User: display active offer with formatted expiration
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding a redemption code system for a hackathon. It is clear, concise, and directly reflects the core changes across frontend (RedeemCouponSection, Settings page, hackathon banner), backend (redemption endpoint, Clerk integration), and security updates (sanitization).
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/redeption-code

Comment @coderabbitai help to get the list of available commands and usage tips.

@bubblelab-pearl
Copy link
Contributor

Suggested PR title from Pearl

Title: feat(promotions): add hackathon coupon redemption system

Body:
Implements a promotional coupon code system for hackathon events that grants temporary Pro access to users.

Frontend Changes

  • Add new Settings page with coupon redemption interface
  • Add Settings navigation link to sidebar
  • Create RedeemCouponSection component for code input and validation
  • Add hackathon offer banner to MonthlyUsageBar showing active promotions
  • Implement useRedeemCoupon hook for API integration

Backend Changes

  • Add /subscription/redeem endpoint for coupon validation and redemption
  • Store hackathon offers in Clerk user private metadata
  • Add auth middleware logic to check active offers and grant Pro Plus access
  • Implement time-based expiration (24 hours from redemption)
  • Add HACKATHON_COUPON_CODES environment variable for code configuration

Features

  • Validates coupon codes against configured list
  • Prevents duplicate redemptions for active offers
  • Displays expiration time in user's local timezone
  • Auto-refreshes subscription data after successful redemption
  • Shows real-time offer status in usage bar and settings

Schema Updates

  • Add HackathonOffer type with activation and expiration tracking
  • Add RedeemCouponRequest and RedeemCouponResponse schemas
  • Extend SubscriptionStatusResponse to include offer information

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/bubble-studio/src/components/RedeemCouponSection.tsx (1)

59-68: Consider extracting date formatting to a shared utility.

The formatExpirationDate function appears to duplicate similar logic mentioned in the AI summary for MonthlyUsageBar.tsx (formatOfferExpiration). Consider extracting this to a shared utility module to maintain consistency and reduce duplication.

🔎 Suggested refactor

Create a shared utility file (e.g., apps/bubble-studio/src/utils/date-formatting.ts):

export function formatOfferExpiration(dateString: string): string {
  const date = new Date(dateString);
  return date.toLocaleString('en-US', {
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    hour12: true,
  });
}

Then import and use in both components:

+import { formatOfferExpiration } from '../utils/date-formatting';
+
 export const RedeemCouponSection: React.FC = () => {
   // ... state and hooks ...
-
-  const formatExpirationDate = (dateString: string): string => {
-    const date = new Date(dateString);
-    return date.toLocaleString('en-US', {
-      month: 'short',
-      day: 'numeric',
-      hour: 'numeric',
-      minute: '2-digit',
-      hour12: true,
-    });
-  };

   return (
     // ... JSX using formatOfferExpiration instead ...
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62b75c5 and 5386bd5.

📒 Files selected for processing (13)
  • apps/bubble-studio/src/components/MonthlyUsageBar.tsx
  • apps/bubble-studio/src/components/RedeemCouponSection.tsx
  • apps/bubble-studio/src/components/Sidebar.tsx
  • apps/bubble-studio/src/hooks/useRedeemCoupon.ts
  • apps/bubble-studio/src/pages/SettingsPage.tsx
  • apps/bubble-studio/src/routeTree.gen.ts
  • apps/bubble-studio/src/routes/settings.tsx
  • apps/bubblelab-api/.env.example
  • apps/bubblelab-api/src/config/env.ts
  • apps/bubblelab-api/src/middleware/auth.ts
  • apps/bubblelab-api/src/routes/subscription.ts
  • apps/bubblelab-api/src/schemas/subscription.ts
  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/packages/bubble-shared-schemas/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/api.mdc)

**/packages/bubble-shared-schemas/**/*.{ts,tsx}: Write shared schemas between frontend and backend in /packages/bubble-shared-schemas directory
Run pnpm build:core after modifying shared schemas since it is a separate package and types need to be regenerated

Files:

  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
🧠 Learnings (5)
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/packages/bubble-shared-schemas/**/*.{ts,tsx} : Write shared schemas between frontend and backend in `/packages/bubble-shared-schemas` directory

Applied to files:

  • apps/bubblelab-api/src/schemas/subscription.ts
  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to packages/bubble-core/src/index.ts : All new types and classes added to bubble-core must be exported from `packages/bubble-core/src/index.ts` to ensure they are included in the generated bundle

Applied to files:

  • apps/bubble-studio/src/pages/SettingsPage.tsx
  • apps/bubble-studio/src/components/RedeemCouponSection.tsx
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to apps/bubble-studio/src/components/MonacoEditor.tsx : The Monaco Editor integration in `apps/bubble-studio/src/components/MonacoEditor.tsx` must fetch the bundled types from `/bubble-types.txt`, wrap them in a module declaration for `bubblelab/bubble-core`, and add them to Monaco's TypeScript type system

Applied to files:

  • apps/bubble-studio/src/components/RedeemCouponSection.tsx
  • apps/bubble-studio/src/routeTree.gen.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: When adding new types to `bubblelab/shared-schemas`, they are automatically included in the bundle without requiring manual configuration changes

Applied to files:

  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to apps/bubble-studio/public/bubble-types.txt : The bundle file at `apps/bubble-studio/public/bubble-types.txt` must be kept synchronized with `packages/bubble-core/dist/bubble-bundle.d.ts` after each build

Applied to files:

  • apps/bubble-studio/src/routeTree.gen.ts
🧬 Code graph analysis (6)
apps/bubblelab-api/src/schemas/subscription.ts (1)
packages/bubble-shared-schemas/src/subscription-status-schema.ts (2)
  • redeemCouponRequestSchema (25-32)
  • redeemCouponResponseSchema (37-52)
apps/bubble-studio/src/pages/SettingsPage.tsx (1)
apps/bubble-studio/src/components/RedeemCouponSection.tsx (1)
  • RedeemCouponSection (6-139)
apps/bubble-studio/src/components/RedeemCouponSection.tsx (2)
apps/bubble-studio/src/hooks/useRedeemCoupon.ts (1)
  • useRedeemCoupon (18-73)
apps/bubble-studio/src/services/analytics.ts (1)
  • reset (107-117)
apps/bubble-studio/src/routes/settings.tsx (1)
apps/bubble-studio/src/pages/SettingsPage.tsx (1)
  • SettingsPage (4-31)
apps/bubblelab-api/src/middleware/auth.ts (1)
apps/bubblelab-api/src/services/subscription-validation.ts (1)
  • PLAN_TYPE (11-11)
apps/bubblelab-api/src/routes/subscription.ts (4)
apps/bubblelab-api/src/middleware/auth.ts (1)
  • getAppType (368-371)
apps/bubblelab-api/src/config/env.ts (1)
  • env (67-95)
packages/bubble-shared-schemas/src/subscription-status-schema.ts (1)
  • HackathonOffer (22-22)
apps/bubblelab-api/src/schemas/subscription.ts (2)
  • redeemCouponRoute (33-66)
  • getSubscriptionStatusRoute (9-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages: bubblelab-documentation
🔇 Additional comments (22)
apps/bubble-studio/src/components/MonthlyUsageBar.tsx (1)

194-202: LGTM!

The hackathon offer banner implementation is clean and follows the existing UI patterns. The conditional rendering logic is sound.

apps/bubble-studio/src/components/Sidebar.tsx (1)

206-243: LGTM!

The Settings navigation item follows the exact same pattern as existing navigation items (Home, My Flows, Credentials). The implementation is consistent and includes proper accessibility attributes.

packages/bubble-shared-schemas/src/subscription-status-schema.ts (2)

4-54: Well-structured schemas for the hackathon feature.

The new schemas are comprehensive and well-documented with OpenAPI annotations. The design appropriately uses nullable types for the offer's expiration fields.


1-139: Reminder: Build the shared schemas package.

After modifying shared schemas, you need to regenerate types by running pnpm build:core from the repository root.

Based on coding guidelines.

apps/bubblelab-api/.env.example (1)

31-31: LGTM!

The new environment variables are properly documented and placed in appropriate sections.

Also applies to: 40-41

apps/bubble-studio/src/pages/SettingsPage.tsx (1)

1-31: LGTM!

Clean and straightforward implementation of the Settings page. The layout structure and styling are consistent with the application's design patterns.

apps/bubble-studio/src/hooks/useRedeemCoupon.ts (1)

1-73: LGTM!

The hook implementation is well-structured with:

  • Proper error handling that extracts structured responses from API errors
  • Query invalidation on successful redemption to refresh subscription data
  • Good logging for debugging
  • Clean TypeScript types
apps/bubble-studio/src/routes/settings.tsx (1)

1-26: LGTM!

The route implementation properly handles authentication by checking isSignedIn and redirecting unauthenticated users to the home page with a sign-in prompt. The redirect pattern follows TanStack Router conventions.

apps/bubblelab-api/src/schemas/subscription.ts (2)

2-6: LGTM! Shared schema imports follow established patterns.

The addition of redeemCouponRequestSchema and redeemCouponResponseSchema from the shared schemas package aligns with the project's architecture for sharing types between frontend and backend.


32-66: LGTM! Route definition is complete and follows best practices.

The redeemCouponRoute definition:

  • Follows the existing pattern established by getSubscriptionStatusRoute
  • Includes proper authentication via BearerAuth
  • Has comprehensive OpenAPI metadata (summary, description, tags)
  • Defines appropriate response codes (200, 400, 401)
  • Uses validated request/response schemas
apps/bubblelab-api/src/config/env.ts (2)

91-91: LGTM! Environment variable follows established patterns.

The HACKATHON_COUPON_CODES environment variable:

  • Uses an empty string as a safe default
  • Follows the same pattern as other optional configuration variables
  • Allows the feature to be disabled by default when not configured

134-136: LGTM! Logging follows security best practices.

The logging statement appropriately shows only whether the variable is configured, without exposing the actual coupon codes. This follows the same secure pattern used for other sensitive configuration values.

apps/bubble-studio/src/components/RedeemCouponSection.tsx (6)

1-16: LGTM! Component structure and imports are well-organized.

The component properly imports necessary dependencies and sets up state management with appropriate hooks for redemption logic and subscription data.


18-31: LGTM! Loading state improves user experience.

The loading state prevents user interaction until subscription data is available and provides clear visual feedback.


33-36: LGTM! Safe offer detection with optional chaining.

The active offer detection properly handles undefined values with optional chaining, preventing potential runtime errors.


37-57: LGTM! Form submission handler is robust.

The handler includes:

  • Proper form event handling
  • Input validation
  • Error state management
  • Success flow (clearing input)
  • Error handling with user-friendly messages

76-86: LGTM! Active offer display provides clear feedback.

The success banner appropriately shows the active offer status with a clear expiration time, using visual cues (green color, CheckCircle icon) for positive reinforcement.


88-136: LGTM! Redemption form has excellent UX.

The form implementation includes several user-friendly features:

  • Automatic uppercase conversion for codes
  • Appropriate disabled states during loading
  • Clear success/error messaging with icons
  • Helpful instructional text
  • Proper visual feedback for all states
apps/bubblelab-api/src/routes/subscription.ts (3)

6-6: LGTM! Import additions support the new redemption feature.

All new imports are necessary for the hackathon coupon functionality and follow the project's module structure.

Also applies to: 19-22, 26-29


36-78: LGTM! Helper functions are well-structured.

The helper functions provide clear separation of concerns:

  • getValidCouponCodes: Safely parses and normalizes comma-separated codes
  • calculateOfferExpiration: Simple 1-day expiration calculation
  • getActiveHackathonOffer: Properly determines offer status with safe null handling

The date parsing in getActiveHackathonOffer handles invalid date strings safely (invalid dates would result in isActive: false).


181-181: LGTM! Status endpoint enhancement has proper error handling.

The hackathon offer integration:

  • Uses appropriate error handling with try-catch
  • Gracefully degrades if Clerk is unavailable (offer simply won't appear)
  • Doesn't impact the core subscription status functionality
  • Properly logs errors for debugging

Also applies to: 246-264, 292-292

apps/bubble-studio/src/routeTree.gen.ts (1)

1-189: LGTM! Generated route tree properly integrates the settings route.

This is an auto-generated file from TanStack Router. The changes consistently integrate the new /settings route following the same patterns as existing routes. The type definitions, interfaces, and module augmentations are all properly structured.

Comment on lines +96 to +106
// Format hackathon offer expiration date
const formatOfferExpiration = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent locale usage in date formatting.

The formatOfferExpiration function uses undefined for the locale parameter, which falls back to the browser's default locale. However, formatResetDate (lines 91-94) explicitly uses 'en-US'. This inconsistency could result in dates being formatted differently across the UI.

🔎 Proposed fix
-  const formatOfferExpiration = (dateString: string): string => {
-    const date = new Date(dateString);
-    return date.toLocaleString(undefined, {
+  const formatOfferExpiration = (dateString: string): string => {
+    const date = new Date(dateString);
+    return date.toLocaleString('en-US', {
       month: 'short',
       day: 'numeric',
       hour: 'numeric',
       minute: '2-digit',
       hour12: true,
     });
   };
📝 Committable suggestion

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

Suggested change
// Format hackathon offer expiration date
const formatOfferExpiration = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
// Format hackathon offer expiration date
const formatOfferExpiration = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
🤖 Prompt for AI Agents
In apps/bubble-studio/src/components/MonthlyUsageBar.tsx around lines 96 to 106,
formatOfferExpiration currently calls toLocaleString with undefined (browser
default) causing inconsistent formatting compared to formatResetDate which uses
'en-US'; change the locale argument to 'en-US' so both functions use the same
explicit locale (preserve the existing options for month, day, hour, minute and
hour12).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a hackathon promotional code redemption feature that allows users to redeem coupon codes for temporary pro access. The implementation includes backend API endpoints for code validation and redemption, schema definitions for the feature, authentication middleware integration to enforce the promotional offers, and a frontend settings page where users can enter and redeem their codes.

  • Backend API endpoint (POST /subscription/redeem) for validating and applying hackathon coupon codes
  • Hackathon offer status integrated into subscription status response and authentication middleware
  • New Settings page in the frontend with a coupon redemption UI component

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/bubble-shared-schemas/src/subscription-status-schema.ts Adds schemas for hackathon offers, coupon redemption requests/responses, and extends subscription status to include offer information
apps/bubblelab-api/src/schemas/subscription.ts Defines OpenAPI route schema for the coupon redemption endpoint
apps/bubblelab-api/src/routes/subscription.ts Implements coupon redemption business logic and integrates offer status into subscription endpoint
apps/bubblelab-api/src/middleware/auth.ts Adds hackathon offer checking to grant pro_plus access when offers are active
apps/bubblelab-api/src/config/env.ts Adds HACKATHON_COUPON_CODES environment variable configuration
apps/bubblelab-api/.env.example Documents the new hackathon coupon codes environment variable
apps/bubble-studio/src/routes/settings.tsx Creates new settings route with authentication guard
apps/bubble-studio/src/pages/SettingsPage.tsx Implements settings page layout with promotions section
apps/bubble-studio/src/hooks/useRedeemCoupon.ts Provides React hook for coupon redemption with mutation handling
apps/bubble-studio/src/components/Sidebar.tsx Adds settings navigation link to sidebar
apps/bubble-studio/src/components/RedeemCouponSection.tsx Implements UI component for entering and redeeming coupon codes
apps/bubble-studio/src/components/MonthlyUsageBar.tsx Displays active hackathon offer status banner
apps/bubble-studio/src/routeTree.gen.ts Auto-generated route tree update for new settings route

}),
message: z.string().openapi({
description: 'Human-readable message about the redemption result',
example: 'Coupon redeemed successfully! You now have Pro access.',
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The error message describes "Pro access" but the code grants "pro_plus" plan with unlimited access. The message should accurately reflect what is being granted - either change the message to "unlimited access" or clarify what "Pro access" means in this context to match the actual plan type being granted.

Suggested change
example: 'Coupon redeemed successfully! You now have Pro access.',
example: 'Coupon redeemed successfully! You now have unlimited access.',

Copilot uses AI. Check for mistakes.
success: false,
message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
},
409
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Using 409 (Conflict) for an already active offer is semantically incorrect. HTTP 409 is typically used for resource conflicts during creation/modification, not for business rule violations. Consider using 400 (Bad Request) instead, which is more appropriate for client errors when attempting an invalid operation.

Suggested change
409
400

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +78
// Helper: Check if hackathon offer is active
function getActiveHackathonOffer(
privateMetadata: Record<string, unknown>
): HackathonOffer | null {
const hackathonOffer = privateMetadata?.hackathonOffer as
| HackathonOfferMetadata
| undefined;

if (!hackathonOffer?.expiresAt) {
return null;
}

const expiresAt = new Date(hackathonOffer.expiresAt);
const isActive = expiresAt > new Date();

return {
isActive,
expiresAt: hackathonOffer.expiresAt,
redeemedAt: hackathonOffer.redeemedAt || null,
};
}
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The function getActiveHackathonOffer guarantees that expiresAt is non-null when it returns a non-null object (due to the check on line 66), but it returns the HackathonOffer type which has expiresAt as nullable. Consider creating a more specific return type like ActiveHackathonOffer that has expiresAt as a required non-null string, which would eliminate the need for the non-null assertion on line 135 and better represent the actual guarantee.

Copilot uses AI. Check for mistakes.
return c.json(
{
success: false,
message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Using toLocaleString() without specifying a locale makes the date format unpredictable across different servers/environments. This could result in inconsistent date formatting in error messages shown to users. Consider using a consistent locale like 'en-US' or using a standardized format like ISO string with custom formatting.

Suggested change
message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
message: `You already have an active promotional offer until ${expirationDate.toLocaleString('en-US')}.`,

Copilot uses AI. Check for mistakes.
Comment on lines 81 to 176
app.openapi(redeemCouponRoute, async (c) => {
const userId = getUserId(c);
const appType = getAppType(c);
const { code } = c.req.valid('json');

// Normalize the code
const normalizedCode = code.trim().toUpperCase();

// Validate the coupon code
const validCodes = getValidCouponCodes();
if (validCodes.length === 0) {
return c.json(
{
success: false,
message: 'Coupon redemption is not available at this time.',
},
503
);
}

if (!validCodes.includes(normalizedCode)) {
return c.json(
{
success: false,
message: 'Invalid coupon code. Please check the code and try again.',
},
400
);
}

// Calculate expiration
const expiresAt = calculateOfferExpiration();
const redeemedAt = new Date();

// Update Clerk private metadata
const clerkClient = getClerkClient(appType);
if (!clerkClient) {
return c.json(
{
success: false,
message: 'Unable to process redemption. Please try again later.',
},
500
);
}

try {
const clerkUser = await clerkClient.users.getUser(userId);

// Check if user already has an active hackathon offer
const existingOffer = getActiveHackathonOffer(
clerkUser.privateMetadata as Record<string, unknown>
);
if (existingOffer?.isActive) {
const expirationDate = new Date(existingOffer.expiresAt!);
return c.json(
{
success: false,
message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
},
409
);
}

// Set the hackathon offer in private metadata
await clerkClient.users.updateUserMetadata(userId, {
privateMetadata: {
...clerkUser.privateMetadata,
hackathonOffer: {
expiresAt: expiresAt.toISOString(),
redeemedAt: redeemedAt.toISOString(),
code: normalizedCode,
},
},
});

console.log(
`[subscription/redeem] User ${userId} redeemed coupon ${normalizedCode}, expires at ${expiresAt.toISOString()}`
);

return c.json({
success: true,
message: 'Coupon redeemed successfully! You now have unlimited access.',
expiresAt: expiresAt.toISOString(),
});
} catch (err) {
console.error('[subscription/redeem] Error redeeming coupon:', err);
return c.json(
{
success: false,
message: 'Failed to redeem coupon. Please try again later.',
},
500
);
}
});
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

There is no rate limiting on the coupon redemption endpoint. This allows attackers to brute-force coupon codes by making unlimited redemption attempts. Consider implementing rate limiting (e.g., max 5 attempts per minute per user) to prevent abuse and protect the coupon codes from being discovered through brute force attacks.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +56
} catch (error) {
// Handle API errors and extract the response data
if (error instanceof ApiHttpError && error.data) {
const errorData = error.data as RedeemCouponResponse;
// If the error response has a valid structure, return it instead of throwing
if (
typeof errorData === 'object' &&
'success' in errorData &&
'message' in errorData
) {
console.log('[useRedeemCoupon] Error response:', errorData);
return errorData;
}
}
throw error;
}
},
onSuccess: (data) => {
if (data.success) {
// Invalidate subscription query to refresh data with new offer
queryClient.invalidateQueries({ queryKey: ['subscription'] });
console.log(
'[useRedeemCoupon] Coupon redeemed, subscription data invalidated'
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The error handling is inconsistent. When the API returns an error response with a valid structure (lines 39-46), it's returned as data rather than thrown as an error. This means errors appear in the data state, not the error state. Then on line 54, there's a catch block that sets a generic error message, which will never execute for API errors since they're not thrown. This makes error handling confusing - consider either throwing all errors consistently or handling all responses (success and error) in the data state.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +141

export default RedeemCouponSection;
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

There's a redundant default export on line 141 when the component is already exported as a named export on line 6. This is unnecessary and could cause confusion about which export style to use. Remove the default export and use only the named export for consistency.

Suggested change
export default RedeemCouponSection;

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +47
// Helper: Calculate expiration (1 day from now)
function calculateOfferExpiration(): Date {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 1); // 1 day from now
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The calculation comment states "1 day from now" but this hardcoded duration should be configurable via environment variable. Hackathon offers might need different durations (e.g., 3 days, 1 week), and hardcoding this makes it inflexible. Consider adding a HACKATHON_OFFER_DURATION_HOURS environment variable.

Suggested change
// Helper: Calculate expiration (1 day from now)
function calculateOfferExpiration(): Date {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 1); // 1 day from now
// Helper: Calculate expiration based on configured duration (default: 24 hours)
function calculateOfferExpiration(): Date {
const expiresAt = new Date();
const durationHoursEnv = env.HACKATHON_OFFER_DURATION_HOURS;
const parsedDuration = durationHoursEnv ? Number(durationHoursEnv) : NaN;
const durationHours =
Number.isFinite(parsedDuration) && parsedDuration > 0 ? parsedDuration : 24;
expiresAt.setTime(
expiresAt.getTime() + durationHours * 60 * 60 * 1000
);

Copilot uses AI. Check for mistakes.
success: false,
message: 'Coupon redemption is not available at this time.',
},
503
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Returning a 503 (Service Unavailable) status code is inappropriate when coupon codes are simply not configured. This status code typically indicates temporary server issues, not missing configuration. Consider using 404 (Not Found) or removing this check entirely and letting the validation fail naturally with an invalid code message.

Suggested change
503
404

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +106
// Format hackathon offer expiration date
const formatOfferExpiration = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

There's code duplication between this formatting function and the one in RedeemCouponSection.tsx (lines 59-68). Both format expiration dates with the same logic. Consider extracting this to a shared utility function to maintain consistency and reduce duplication.

Copilot uses AI. Check for mistakes.
@zhubzy zhubzy force-pushed the feat/redeption-code branch from 5386bd5 to 6aca24f Compare December 22, 2025 20:43
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 22, 2025

Deploying bubblelab-documentation with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5ab85e3
Status: ✅  Deploy successful!
Preview URL: https://2e35d2fd.bubblelab-documentation.pages.dev
Branch Preview URL: https://feat-redeption-code.bubblelab-documentation.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 22, 2025

Deploying bubble-studio with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5ab85e3
Status: ✅  Deploy successful!
Preview URL: https://e660ec3a.bubble-studio.pages.dev
Branch Preview URL: https://feat-redeption-code.bubble-studio.pages.dev

View logs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (10)
apps/bubble-studio/src/components/MonthlyUsageBar.tsx (2)

96-106: Extract duplicate date formatting logic to a shared utility.

This formatOfferExpiration function duplicates the same formatting logic in RedeemCouponSection.tsx (lines 59-68). Extracting this to a shared utility function would reduce duplication and ensure consistent formatting across components.

🔎 Proposed refactor

Create a new utility file apps/bubble-studio/src/utils/date-formatting.ts:

export function formatOfferExpiration(dateString: string): string {
  const date = new Date(dateString);
  return date.toLocaleString('en-US', {
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    hour12: true,
  });
}

Then import and use it in both components:

+import { formatOfferExpiration } from '@/utils/date-formatting';
+
-  const formatOfferExpiration = (dateString: string): string => {
-    const date = new Date(dateString);
-    return date.toLocaleString(undefined, {
-      month: 'short',
-      day: 'numeric',
-      hour: 'numeric',
-      minute: '2-digit',
-      hour12: true,
-    });
-  };

96-106: Inconsistent locale usage in date formatting.

The formatOfferExpiration function uses undefined for the locale parameter, which falls back to the browser's default locale. However, formatResetDate (lines 91-94) explicitly uses 'en-US'. This inconsistency could result in dates being formatted differently across the UI.

🔎 Proposed fix
 const formatOfferExpiration = (dateString: string): string => {
   const date = new Date(dateString);
-  return date.toLocaleString(undefined, {
+  return date.toLocaleString('en-US', {
     month: 'short',
     day: 'numeric',
     hour: 'numeric',
     minute: '2-digit',
     hour12: true,
   });
 };
apps/bubblelab-api/src/routes/subscription.ts (7)

44-49: Make the offer duration configurable via environment variable.

The expiration duration is hardcoded to 1 day (line 47). Different hackathon offers might require different durations (e.g., 3 days, 1 week). Consider adding a HACKATHON_OFFER_DURATION_HOURS environment variable for flexibility.

🔎 Proposed refactor

In apps/bubblelab-api/src/config/env.ts, add:

HACKATHON_OFFER_DURATION_HOURS: process.env.HACKATHON_OFFER_DURATION_HOURS || '24',

Then update this function:

-// Helper: Calculate expiration (1 day from now)
+// Helper: Calculate expiration based on configured duration (default: 24 hours)
 function calculateOfferExpiration(): Date {
   const expiresAt = new Date();
-  expiresAt.setDate(expiresAt.getDate() + 1); // 1 day from now
+  const durationHoursEnv = env.HACKATHON_OFFER_DURATION_HOURS;
+  const parsedDuration = durationHoursEnv ? Number(durationHoursEnv) : NaN;
+  const durationHours =
+    Number.isFinite(parsedDuration) && parsedDuration > 0 ? parsedDuration : 24;
+  expiresAt.setTime(
+    expiresAt.getTime() + durationHours * 60 * 60 * 1000
+  );
   return expiresAt;
 }

58-78: Consider a more specific return type for active offers.

The function getActiveHackathonOffer guarantees that expiresAt is non-null when it returns a non-null object (due to the check on line 66), but it returns the HackathonOffer type which has expiresAt as nullable. A more specific return type like ActiveHackathonOffer with required non-null expiresAt would eliminate the need for the non-null assertion on line 135 and better represent the actual guarantee.

🔎 Proposed refactor
+// Helper: Active hackathon offer with guaranteed non-null expiresAt
+interface ActiveHackathonOffer extends HackathonOffer {
+  expiresAt: string; // non-null override
+}
+
 // Helper: Check if hackathon offer is active
 function getActiveHackathonOffer(
   privateMetadata: Record<string, unknown>
-): HackathonOffer | null {
+): ActiveHackathonOffer | null {
   const hackathonOffer = privateMetadata?.hackathonOffer as
     | HackathonOfferMetadata
     | undefined;

80-176: Add rate limiting to prevent coupon code brute-force attacks.

The redemption endpoint lacks rate limiting, which could allow attackers to brute-force coupon codes by making unlimited redemption attempts. Consider implementing rate limiting (e.g., max 5 attempts per minute per user) using middleware or a library like hono-rate-limiter to protect against abuse.


91-99: Use a more appropriate HTTP status code for missing configuration.

Returning 503 (Service Unavailable) when coupon codes are not configured is inappropriate. This status code typically indicates temporary server issues, not missing configuration. Consider using 404 (Not Found) to indicate the feature is unavailable, or let the validation fail naturally with an invalid code message.

🔎 Proposed fix
   if (validCodes.length === 0) {
     return c.json(
       {
         success: false,
         message: 'Coupon redemption is not available at this time.',
       },
-      503
+      404
     );
   }

134-143: Use 400 (Bad Request) for business rule violations.

Using 409 (Conflict) for an already active offer is semantically incorrect. HTTP 409 is typically used for resource conflicts during creation/modification (e.g., duplicate resource), not for business rule violations. Use 400 (Bad Request) instead, which is more appropriate for client errors when attempting an invalid operation.

🔎 Proposed fix
       return c.json(
         {
           success: false,
           message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
         },
-        409
+        400
       );

139-139: Use a consistent locale for date formatting.

Using toLocaleString() without specifying a locale makes the date format unpredictable across different servers/environments. This could result in inconsistent date formatting in error messages shown to users. Consider using a consistent locale like 'en-US' or a standardized ISO format with custom formatting.

🔎 Proposed fix
-          message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
+          message: `You already have an active promotional offer until ${expirationDate.toLocaleString('en-US')}.`,

163-163: Clarify the duration in the success message.

The message "You now have unlimited access" could be misleading since the offer expires in 1 day. Be more specific about the duration to set proper user expectations.

🔎 Suggested improvement
     return c.json({
       success: true,
-      message: 'Coupon redeemed successfully! You now have unlimited access.',
+      message: 'Coupon redeemed successfully! You now have 1 day of unlimited access.',
       expiresAt: expiresAt.toISOString(),
     });
packages/bubble-shared-schemas/src/subscription-status-schema.ts (1)

36-54: Clarify the success message to accurately reflect what is granted.

The message on line 45 states "You now have Pro access" but the actual implementation grants "pro_plus" plan with unlimited access for a limited time (24 hours). The message should accurately reflect what is being granted to avoid user confusion.

🔎 Proposed fix
     message: z.string().openapi({
       description: 'Human-readable message about the redemption result',
-      example: 'Coupon redeemed successfully! You now have Pro access.',
+      example: 'Coupon redeemed successfully! You now have unlimited access.',
     }),
🧹 Nitpick comments (2)
apps/bubblelab-api/.env.example (1)

40-41: Consider section placement and add example format for HACKATHON_COUPON_CODES.

HACKATHON_COUPON_CODES is consumed by the backend /subscription/redeem endpoint for coupon validation, not by Python scripts. The current placement under "For python scripts / pdf operations" may be slightly misleading. Consider either:

  1. Moving it to a new dedicated section for hackathon/promotion configuration, or
  2. Moving it under "Optional environmental variables for default integration support"

Additionally, include an example format to guide developers on the expected comma-separated structure.

🔎 Proposed refactor for improved organization and clarity
- # For python scripts / pdf operations
+ # For python scripts / pdf operations
  PYTHON_PATH=
  
+ # Hackathon / Promotions
+ HACKATHON_COUPON_CODES=CODE1,CODE2,CODE3 # Comma-separated list of valid hackathon coupon codes

Alternatively, move it under the optional environmental variables section:

  # Optional environmental variables for default integration support
  OPENAI_API_KEY=
  RESEND_API_KEY=
  FIRE_CRAWL_API_KEY=
  CLOUDFLARE_R2_ACCESS_KEY=
  CLOUDFLARE_R2_SECRET_KEY=
  CLOUDFLARE_R2_ACCOUNT_ID=
  WISPR_API_KEY=
+ HACKATHON_COUPON_CODES= # Comma-separated list of valid hackathon coupon codes (e.g., CODE1,CODE2,CODE3)
packages/bubble-runtime/src/runtime/BubbleRunner.test.ts (1)

450-457: Consider consistency with expectValidScript or document the intent.

This test uses validateBubbleFlow directly with requireLintErrors=true, whereas expectValidScript uses requireLintErrors=false. If the stricter lint validation is intentional for this specific test case, consider adding a brief comment explaining why. Otherwise, consider using expectValidScript for consistency.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5386bd5 and 6aca24f.

📒 Files selected for processing (21)
  • apps/bubble-studio/src/components/MonthlyUsageBar.tsx
  • apps/bubble-studio/src/components/RedeemCouponSection.tsx
  • apps/bubble-studio/src/components/Sidebar.tsx
  • apps/bubble-studio/src/hooks/useRedeemCoupon.ts
  • apps/bubble-studio/src/pages/SettingsPage.tsx
  • apps/bubble-studio/src/routeTree.gen.ts
  • apps/bubble-studio/src/routes/settings.tsx
  • apps/bubblelab-api/.env.example
  • apps/bubblelab-api/src/config/env.ts
  • apps/bubblelab-api/src/middleware/auth.ts
  • apps/bubblelab-api/src/routes/subscription.ts
  • apps/bubblelab-api/src/schemas/subscription.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
  • packages/bubble-runtime/src/utils/sanitize-script.ts
  • packages/bubble-runtime/tests/fixtures/index.ts
  • packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-env-bracket.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-env-standalone.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-env.ts
  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/bubble-studio/src/components/Sidebar.tsx
  • apps/bubble-studio/src/hooks/useRedeemCoupon.ts
  • apps/bubblelab-api/src/middleware/auth.ts
  • apps/bubblelab-api/src/config/env.ts
  • apps/bubble-studio/src/components/RedeemCouponSection.tsx
  • apps/bubblelab-api/src/schemas/subscription.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/packages/bubble-shared-schemas/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/api.mdc)

**/packages/bubble-shared-schemas/**/*.{ts,tsx}: Write shared schemas between frontend and backend in /packages/bubble-shared-schemas directory
Run pnpm build:core after modifying shared schemas since it is a separate package and types need to be regenerated

Files:

  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
🧠 Learnings (13)
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to packages/bubble-core/scripts/bubble-bundler.ts : The bundler script at `packages/bubble-core/scripts/bubble-bundler.ts` must use absolute file paths as cache keys to prevent collisions between packages with identically named files

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
  • packages/bubble-runtime/src/utils/sanitize-script.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to packages/bubble-core/dist/**/*.d.ts : All external imports (except relative imports) must be removed from the bundled output to prevent circular processing and external dependency issues

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to packages/bubble-core/dist/**/*.d.ts : The type bundling system for Monaco Editor must process TypeScript declaration files (.d.ts) and inline all dependencies into a single self-contained bundle in `packages/bubble-core/dist/bubble-bundle.d.ts`

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-env-standalone.ts
  • packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: The bundle process must run the TypeScript compiler (`tsc`) before executing the bundler script (`tsx scripts/bubble-bundler.ts`) to ensure all .d.ts files are generated

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to packages/bubble-core/src/index.ts : All new types and classes added to bubble-core must be exported from `packages/bubble-core/src/index.ts` to ensure they are included in the generated bundle

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
  • apps/bubble-studio/src/pages/SettingsPage.tsx
  • packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-22T09:55:47.864Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bubblelab.mdc:0-0
Timestamp: 2025-12-22T09:55:47.864Z
Learning: Applies to packages/bubble-runtime/src/extraction/README.md : Refer to packages/bubble-runtime/src/extraction/README.md for information about bubble parsing, dependency graphs, and per-invocation cloning

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.ts
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/bubblelab-api/**/*.test.{js,ts} : Reference webhook.test as the example for how backend tests should be written

Applied to files:

  • packages/bubble-runtime/tests/fixtures/malicious-process-env-standalone.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-env.ts
  • packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-env-bracket.ts
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/bubblelab-api/**/*.test.{js,ts} : Use `pnpm bun test` command to run backend tests to ensure setup files are properly loaded

Applied to files:

  • packages/bubble-runtime/tests/fixtures/malicious-process-env-standalone.ts
  • packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-22T09:55:47.864Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bubblelab.mdc:0-0
Timestamp: 2025-12-22T09:55:47.864Z
Learning: Applies to packages/bubble-shared-schemas/src/bubbleflow-generation-prompts.ts : Refer to packages/bubble-shared-schemas/src/bubbleflow-generation-prompts.ts for documentation on how bubble flow is supposed to be generated

Applied to files:

  • packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/packages/bubble-shared-schemas/**/*.{ts,tsx} : Write shared schemas between frontend and backend in `/packages/bubble-shared-schemas` directory

Applied to files:

  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: When adding new types to `bubblelab/shared-schemas`, they are automatically included in the bundle without requiring manual configuration changes

Applied to files:

  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/packages/bubble-shared-schemas/**/*.{ts,tsx} : Run `pnpm build:core` after modifying shared schemas since it is a separate package and types need to be regenerated

Applied to files:

  • packages/bubble-shared-schemas/src/subscription-status-schema.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to apps/bubble-studio/public/bubble-types.txt : The bundle file at `apps/bubble-studio/public/bubble-types.txt` must be kept synchronized with `packages/bubble-core/dist/bubble-bundle.d.ts` after each build

Applied to files:

  • apps/bubble-studio/src/routeTree.gen.ts
🧬 Code graph analysis (10)
packages/bubble-runtime/src/runtime/BubbleRunner.ts (2)
packages/bubble-runtime/src/utils/sanitize-script.ts (1)
  • sanitizeScript (10-53)
packages/bubble-runtime/src/index.ts (1)
  • sanitizeScript (8-8)
apps/bubble-studio/src/pages/SettingsPage.tsx (1)
apps/bubble-studio/src/components/RedeemCouponSection.tsx (1)
  • RedeemCouponSection (6-139)
packages/bubble-runtime/tests/fixtures/malicious-process-env-standalone.ts (2)
packages/bubble-shared-schemas/src/trigger.ts (1)
  • WebhookEvent (64-66)
packages/bubble-core/src/index.ts (1)
  • BubbleFlow (19-19)
packages/bubble-runtime/src/utils/sanitize-script.ts (1)
packages/bubble-runtime/src/index.ts (1)
  • sanitizeScript (8-8)
packages/bubble-runtime/tests/fixtures/malicious-process-env.ts (2)
packages/bubble-shared-schemas/src/trigger.ts (1)
  • WebhookEvent (64-66)
packages/bubble-core/src/index.ts (1)
  • BubbleFlow (19-19)
packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts (2)
packages/bubble-shared-schemas/src/trigger.ts (1)
  • WebhookEvent (64-66)
packages/bubble-core/src/index.ts (1)
  • BubbleFlow (19-19)
packages/bubble-runtime/src/runtime/BubbleRunner.test.ts (3)
packages/bubble-runtime/src/runtime/BubbleRunner.ts (1)
  • BubbleRunner (47-791)
packages/bubble-runtime/src/validation/index.ts (1)
  • validateBubbleFlow (37-102)
packages/bubble-runtime/tests/fixtures/index.ts (1)
  • getFixture (88-90)
apps/bubblelab-api/src/routes/subscription.ts (4)
apps/bubblelab-api/src/middleware/auth.ts (2)
  • getUserId (320-329)
  • getAppType (368-371)
apps/bubblelab-api/src/config/env.ts (1)
  • env (67-95)
packages/bubble-shared-schemas/src/subscription-status-schema.ts (1)
  • HackathonOffer (22-22)
apps/bubblelab-api/src/schemas/subscription.ts (1)
  • redeemCouponRoute (33-66)
packages/bubble-runtime/tests/fixtures/malicious-process-env-bracket.ts (2)
packages/bubble-shared-schemas/src/trigger.ts (1)
  • WebhookEvent (64-66)
packages/bubble-core/src/index.ts (1)
  • BubbleFlow (19-19)
apps/bubble-studio/src/routes/settings.tsx (1)
apps/bubble-studio/src/pages/SettingsPage.tsx (1)
  • SettingsPage (4-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages: bubblelab-documentation
🔇 Additional comments (18)
apps/bubble-studio/src/components/MonthlyUsageBar.tsx (1)

194-202: LGTM!

The hackathon offer banner is correctly implemented with proper conditional rendering and uses the formatted expiration date effectively.

packages/bubble-shared-schemas/src/subscription-status-schema.ts (3)

4-22: LGTM!

The hackathonOfferSchema is well-structured with appropriate OpenAPI metadata and clear field descriptions.


24-34: LGTM!

The redeemCouponRequestSchema correctly validates coupon codes with a minimum length constraint.


131-133: LGTM! Don't forget to rebuild the shared schemas.

The hackathonOffer field is correctly integrated into the subscription status response schema.

As per coding guidelines: Run pnpm build:core after modifying shared schemas since it is a separate package and types need to be regenerated.

apps/bubblelab-api/src/routes/subscription.ts (2)

36-42: LGTM!

The getValidCouponCodes function correctly parses and normalizes coupon codes from the environment variable.


246-264: LGTM!

The hackathon offer status retrieval is correctly implemented with proper error handling and logging.

packages/bubble-runtime/src/runtime/BubbleRunner.ts (1)

426-427: LGTM!

The call to sanitizeScript has been correctly updated to match the new signature that accepts only the script code. This aligns with the security improvements to block process.env access.

apps/bubble-studio/src/routes/settings.tsx (1)

9-26: LGTM!

The authentication check and redirect logic are correctly implemented. The use of replace: true prevents back-button issues, and returning null during the redirect is appropriate for React 19.

apps/bubble-studio/src/pages/SettingsPage.tsx (1)

1-31: LGTM!

The SettingsPage component is well-structured with clean layout and proper integration of the RedeemCouponSection. The styling is consistent with the application's design patterns.

packages/bubble-runtime/tests/fixtures/malicious-process-env-standalone.ts (1)

1-26: LGTM!

This test fixture correctly demonstrates the malicious patterns that the sanitizeScript function should block. The code attempts to access process.env directly (line 16) and enumerate its keys (line 17), which are appropriate test cases for validating security controls.

packages/bubble-runtime/tests/fixtures/index.ts (1)

66-69: LGTM!

The new test fixtures for process.env security testing are correctly integrated. The naming is clear and follows the existing pattern, making it easy to identify malicious vs. legitimate test cases.

packages/bubble-runtime/tests/fixtures/malicious-process-env-bracket.ts (1)

1-26: LGTM!

The test fixture correctly demonstrates the bracket notation attack vector for process.env access. The structure is consistent with the other malicious fixtures, and it properly tests both single and multiple key accesses via bracket notation.

packages/bubble-runtime/src/runtime/BubbleRunner.test.ts (2)

7-30: LGTM! Good helper extraction.

The expectValidScript helper consolidates repeated validation logic into a single reusable function with optional error logging. This improves test maintainability and reduces duplication.


565-652: LGTM! Comprehensive security test coverage.

The test suite thoroughly covers all process.env access vectors:

  • Dot notation (process.env.KEY)
  • Bracket notation (process.env['KEY'])
  • Standalone object access (process.env)
  • Legitimate string/comment mentions (should pass)

The tests properly validate script syntax before execution and assert the expected security error message.

packages/bubble-runtime/src/utils/sanitize-script.ts (1)

10-53: LGTM! Well-designed two-pass sanitization.

The placeholder-based approach correctly protects string literals and comments from false positive matches. The split error message ('process' + '.env') cleverly avoids self-matching during the regex replacement.

Minor edge case consideration: If user code contains __SANITIZE_PLACEHOLDER_ followed by a number and __, the restoration loop could cause unexpected behavior. Consider using a more unique/random prefix if this becomes a concern in practice.

packages/bubble-runtime/tests/fixtures/malicious-process-env.ts (1)

1-25: LGTM!

The test fixture correctly demonstrates the dot notation attack vector (process.env.SECRET_KEY). The structure is consistent with the other test fixtures in this PR.

packages/bubble-runtime/tests/fixtures/legitimate-process-env-string.ts (1)

1-28: LGTM!

Excellent positive test case that validates the sanitizer correctly preserves legitimate mentions of process.env in:

  • Single-line comments (lines 1, 13, 16)
  • String literals (line 18)
  • Template strings (line 19)

This ensures the two-pass placeholder approach works correctly without false positives.

apps/bubble-studio/src/routeTree.gen.ts (1)

12-12: LGTM! Auto-generated route integration is correct.

The SettingsRoute has been correctly integrated into the generated route tree. All additions follow the established pattern for TanStack Router route registration, including:

  • Import declaration
  • Route constant creation
  • Type declarations across all route interfaces
  • Module augmentation for type safety
  • Route tree wiring

As noted in the file header, this is an auto-generated file and should not be manually modified.

Also applies to: 20-24, 62-62, 71-71, 81-81, 92-92, 95-102, 110-110, 120-120, 126-132, 184-184

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/bubblelab-api/src/routes/subscription.ts (3)

149-158: Inconsistent date formatting: use locale for consistency.

formatExpirationDate (line 39) uses 'en-US' locale, but line 154 uses toLocaleString() without a locale, making the date format environment-dependent.

🔎 Proposed fix
     if (existingOffer?.isActive) {
       const expirationDate = new Date(existingOffer.expiresAt!);
       return c.json(
         {
           success: false,
-          message: `You already have an active promotional offer until ${expirationDate.toLocaleString()}.`,
+          message: `You already have an active promotional offer until ${formatExpirationDate(expirationDate)}.`,
         },
         409
       );
     }

106-114: Consider using 404 instead of 503 for unconfigured coupon codes.

503 (Service Unavailable) typically indicates temporary server issues. If coupon codes aren't configured, this is more of a "feature not available" scenario. A 404 or disabling the endpoint entirely might be more semantically correct, though 503 is acceptable if this is meant to be a temporary state during hackathon setup.


70-90: Consider tightening the return type to avoid non-null assertion.

The function guarantees expiresAt is non-null when returning a non-null object (due to the check on line 78), but the return type uses HackathonOffer which may have nullable expiresAt. This forces the non-null assertion on line 150.

🔎 Suggested improvement
+// Return type guarantees expiresAt is present
+interface ActiveHackathonOffer extends Omit<HackathonOffer, 'expiresAt'> {
+  expiresAt: string; // non-null guaranteed
+}

 // Helper: Check if hackathon offer is active
 function getActiveHackathonOffer(
   privateMetadata: Record<string, unknown>
-): HackathonOffer | null {
+): ActiveHackathonOffer | null {
🧹 Nitpick comments (3)
packages/bubble-runtime/src/runtime/BubbleRunner.test.ts (1)

7-30: Clarify the intended usage pattern for expectValidScript.

The docstring states "This should be called after runAll()," but the new security tests (lines 579, 602, 625, 648, 671) call it before runAll(), while other tests (lines 431, 444, 468, etc.) follow the docstring and call it after. This inconsistency creates confusion about the correct usage pattern.

If calling it before runAll() is intentional for security tests (e.g., to validate that sanitization produces syntactically valid code before execution), consider updating the docstring to clarify that both patterns are valid depending on the test scenario.

Suggested docstring update
 /**
  * Utility function to validate that a BubbleRunner's script is valid after execution.
- * This should be called after runAll() to ensure the injected code didn't break the script.
+ * Can be called before runAll() to validate sanitization transforms, or after runAll() 
+ * to ensure the injected code didn't break the script.
  */
apps/bubblelab-api/src/middleware/rate-limit.test.ts (1)

6-152: Good test coverage with room for edge case improvement.

The tests cover the core rate limiting behaviors well with proper isolation via unique keyGenerator keys. Consider adding a test for window reset behavior to verify that requests are allowed again after windowMs expires:

🔎 Suggested additional test case
it('should reset counter after window expires', async () => {
  const shortWindow = 100; // 100ms for testing
  app.use(
    '/test',
    rateLimit({
      windowMs: shortWindow,
      maxAttempts: 1,
      keyGenerator: () => 'test-user-reset',
    })
  );
  app.post('/test', (c) => c.json({ success: true }));

  // First request succeeds
  const res1 = await app.request('/test', { method: 'POST' });
  expect(res1.status).toBe(200);

  // Second request blocked
  const res2 = await app.request('/test', { method: 'POST' });
  expect(res2.status).toBe(429);

  // Wait for window to expire
  await new Promise((resolve) => setTimeout(resolve, shortWindow + 10));

  // Third request should succeed (window reset)
  const res3 = await app.request('/test', { method: 'POST' });
  expect(res3.status).toBe(200);
});

Based on learnings, tests should reference webhook.test as the example pattern. Run tests with pnpm bun test to ensure setup files are properly loaded.

apps/bubblelab-api/src/middleware/rate-limit.ts (1)

58-64: First request doesn't receive rate limit headers.

When a key has no existing entry, the middleware sets the count and calls next() without adding X-RateLimit-* headers. This creates inconsistency where clients only see rate limit headers after their second request (or when blocked).

🔎 Proposed fix to add headers on first request
     if (!entry) {
       // First request in window
-      rateLimitStore.set(key, {
+      const newEntry = {
         count: 1,
         resetAt: now + windowMs,
-      });
+      };
+      rateLimitStore.set(key, newEntry);
+
+      // Add rate limit headers for first request
+      c.header('X-RateLimit-Limit', maxAttempts.toString());
+      c.header('X-RateLimit-Remaining', (maxAttempts - 1).toString());
+      c.header('X-RateLimit-Reset', Math.ceil(newEntry.resetAt / 1000).toString());
+
       return next();
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6aca24f and 5ab85e3.

📒 Files selected for processing (7)
  • apps/bubblelab-api/src/middleware/rate-limit.test.ts
  • apps/bubblelab-api/src/middleware/rate-limit.ts
  • apps/bubblelab-api/src/routes/subscription.ts
  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
  • packages/bubble-runtime/src/utils/sanitize-script.ts
  • packages/bubble-runtime/tests/fixtures/index.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-bracket-env.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/bubble-runtime/src/utils/sanitize-script.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/bubblelab-api/**/*.test.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/api.mdc)

**/bubblelab-api/**/*.test.{js,ts}: Use pnpm bun test command to run backend tests to ensure setup files are properly loaded
Reference webhook.test as the example for how backend tests should be written

Files:

  • apps/bubblelab-api/src/middleware/rate-limit.test.ts
🧠 Learnings (4)
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/bubblelab-api/**/*.test.{js,ts} : Use `pnpm bun test` command to run backend tests to ensure setup files are properly loaded

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
  • apps/bubblelab-api/src/middleware/rate-limit.test.ts
📚 Learning: 2025-12-19T03:17:06.817Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bundling.mdc:0-0
Timestamp: 2025-12-19T03:17:06.817Z
Learning: Applies to packages/bubble-core/scripts/bubble-bundler.ts : The bundler script at `packages/bubble-core/scripts/bubble-bundler.ts` must use absolute file paths as cache keys to prevent collisions between packages with identically named files

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
📚 Learning: 2025-12-19T03:16:48.793Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/api.mdc:0-0
Timestamp: 2025-12-19T03:16:48.793Z
Learning: Applies to **/bubblelab-api/**/*.test.{js,ts} : Reference webhook.test as the example for how backend tests should be written

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
  • apps/bubblelab-api/src/middleware/rate-limit.test.ts
  • packages/bubble-runtime/tests/fixtures/malicious-process-bracket-env.ts
📚 Learning: 2025-12-22T09:55:47.864Z
Learnt from: CR
Repo: bubblelabai/BubbleLab PR: 0
File: .cursor/rules/bubblelab.mdc:0-0
Timestamp: 2025-12-22T09:55:47.864Z
Learning: Applies to packages/bubble-shared-schemas/src/bubbleflow-generation-prompts.ts : Refer to packages/bubble-shared-schemas/src/bubbleflow-generation-prompts.ts for documentation on how bubble flow is supposed to be generated

Applied to files:

  • packages/bubble-runtime/src/runtime/BubbleRunner.test.ts
🧬 Code graph analysis (4)
packages/bubble-runtime/src/runtime/BubbleRunner.test.ts (2)
packages/bubble-runtime/src/runtime/BubbleRunner.ts (1)
  • BubbleRunner (47-791)
packages/bubble-runtime/src/validation/index.ts (1)
  • validateBubbleFlow (37-102)
apps/bubblelab-api/src/middleware/rate-limit.test.ts (1)
apps/bubblelab-api/src/middleware/rate-limit.ts (1)
  • rateLimit (36-96)
apps/bubblelab-api/src/routes/subscription.ts (4)
apps/bubblelab-api/src/middleware/auth.ts (2)
  • getUserId (320-329)
  • getAppType (368-371)
apps/bubblelab-api/src/config/env.ts (1)
  • env (67-95)
packages/bubble-shared-schemas/src/subscription-status-schema.ts (1)
  • HackathonOffer (22-22)
apps/bubblelab-api/src/middleware/rate-limit.ts (1)
  • couponRedemptionRateLimit (102-106)
packages/bubble-runtime/tests/fixtures/malicious-process-bracket-env.ts (2)
packages/bubble-shared-schemas/src/trigger.ts (1)
  • WebhookEvent (64-66)
packages/bubble-core/src/index.ts (1)
  • BubbleFlow (19-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: Cloudflare Pages: bubblelab-documentation
🔇 Additional comments (7)
packages/bubble-runtime/tests/fixtures/malicious-process-bracket-env.ts (1)

1-26: LGTM! Well-structured security test fixture.

The fixture correctly demonstrates the malicious pattern it's designed to test, using both pure bracket notation (process['env']['SECRET_KEY']) and mixed bracket/dot notation (process['env'].API_TOKEN). This complements the other security fixtures to ensure comprehensive coverage of process.env access patterns.

packages/bubble-runtime/src/runtime/BubbleRunner.test.ts (1)

568-678: Excellent security test coverage!

The new test suite comprehensively validates process.env access prevention across multiple attack vectors:

  • Dot notation, bracket notation, standalone object access, and mixed patterns
  • A positive test case ensuring legitimate string/comment mentions aren't blocked

The consistent error assertion pattern and thorough coverage strengthen the security posture of the runtime.

packages/bubble-runtime/tests/fixtures/index.ts (1)

66-70: All new fixture files are properly created and registered.

The five new security test fixtures are in place:

  • malicious-process-env.ts
  • malicious-process-env-bracket.ts
  • malicious-process-env-standalone.ts
  • malicious-process-bracket-env.ts
  • legitimate-process-env-string.ts
apps/bubblelab-api/src/middleware/rate-limit.ts (1)

15-29: In-memory rate limiting won't scale across multiple server instances.

The rateLimitStore Map is local to each process. In a multi-instance deployment, rate limits won't be shared, allowing users to exceed limits by hitting different instances. For the hackathon feature this is likely acceptable, but consider documenting this limitation or using Redis for production-grade rate limiting.

apps/bubblelab-api/src/routes/subscription.ts (3)

37-46: LGTM: Good use of explicit locale for consistent date formatting.

Using 'en-US' locale ensures consistent date formatting across different server environments.


160-170: Good practice: Preserving existing metadata when updating.

The spread of clerkUser.privateMetadata before adding hackathonOffer ensures other private metadata fields aren't overwritten.


261-279: LGTM: Good error handling for Clerk access in status endpoint.

The try/catch with logging ensures the status endpoint doesn't fail if hackathon offer retrieval fails.

Comment on lines +98 to +106
/**
* Pre-configured rate limiter for coupon redemption
* 5 attempts per hour per user
*/
export const couponRedemptionRateLimit = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
maxAttempts: 10,
message: 'Too many redemption attempts.',
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment/code mismatch: documentation says 5 attempts but code configures 10.

The JSDoc comment states "5 attempts per hour" but maxAttempts is set to 10.

🔎 Proposed fix
 /**
  * Pre-configured rate limiter for coupon redemption
- * 5 attempts per hour per user
+ * 10 attempts per hour per user
  */
 export const couponRedemptionRateLimit = rateLimit({
   windowMs: 60 * 60 * 1000, // 1 hour
   maxAttempts: 10,
   message: 'Too many redemption attempts.',
 });
📝 Committable suggestion

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

Suggested change
/**
* Pre-configured rate limiter for coupon redemption
* 5 attempts per hour per user
*/
export const couponRedemptionRateLimit = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
maxAttempts: 10,
message: 'Too many redemption attempts.',
});
/**
* Pre-configured rate limiter for coupon redemption
* 10 attempts per hour per user
*/
export const couponRedemptionRateLimit = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
maxAttempts: 10,
message: 'Too many redemption attempts.',
});
🤖 Prompt for AI Agents
In apps/bubblelab-api/src/middleware/rate-limit.ts around lines 98 to 106, the
JSDoc says "5 attempts per hour" but the rate limiter is configured with
maxAttempts: 10; update them to be consistent by setting maxAttempts to 5 to
match the comment (or if 10 is intended, change the JSDoc comment to reflect "10
attempts per hour"); ensure the chosen value is applied to the rateLimit config
and the JSDoc text is updated accordingly.

Comment on lines +92 to +93
// Apply rate limiting to redeem endpoint (5 attempts per hour per user)
app.use('/redeem', couponRedemptionRateLimit);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment states "5 attempts per hour" but rate limiter is configured for 10.

This comment is inconsistent with the actual couponRedemptionRateLimit configuration (10 attempts per hour).

🔎 Proposed fix
-// Apply rate limiting to redeem endpoint (5 attempts per hour per user)
+// Apply rate limiting to redeem endpoint (10 attempts per hour per user)
 app.use('/redeem', couponRedemptionRateLimit);
📝 Committable suggestion

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

Suggested change
// Apply rate limiting to redeem endpoint (5 attempts per hour per user)
app.use('/redeem', couponRedemptionRateLimit);
// Apply rate limiting to redeem endpoint (10 attempts per hour per user)
app.use('/redeem', couponRedemptionRateLimit);
🤖 Prompt for AI Agents
In apps/bubblelab-api/src/routes/subscription.ts around lines 92-93, the inline
comment states "5 attempts per hour per user" but the actual
couponRedemptionRateLimit is configured for 10 attempts per hour; update the
comment to accurately reflect the configured limit (change "5" to "10") or, if
the intended limit is 5, modify the rate limiter configuration to 5 attempts per
hour so code and comment match.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants