Skip to content

Conversation

AbanoubGhadban
Copy link
Collaborator

@AbanoubGhadban AbanoubGhadban commented Oct 8, 2025

Summary

This PR implements a comprehensive offline license validation system and attribution comments for React on Rails Pro.

Key Features

  • Offline JWT license validation using RSA-256 signatures
  • Production grace period (1 month) for ANY expired license in production
  • Attribution comments in rendered HTML showing license status
  • Dual validation: Ruby gem (startup) + Node renderer (startup)
  • No internet required: Validation uses embedded public keys

Motivation

React on Rails Pro needs a licensing system that:

  • Works offline: No phone-home or internet dependency
  • Provides flexibility: FREE evaluation licenses for dev/test/CI (duration in JWT)
  • Protects production: Grace period prevents sudden outages when licenses expire
  • Is transparent: Attribution comments show what's running

Implementation Details

License Validation

API Design (Ruby & Node.js are aligned):

Both Ruby and Node.js implementations use the same API pattern:

  • Primary method: getValidatedLicenseData() / validated_license_data!
    • Validates license and returns data
    • Caches result for performance
    • Exits process if invalid
  • Helper methods: isEvaluation() / evaluation?, getGraceDaysRemaining() / grace_days_remaining
  • Testing method: reset() / reset! to clear cache

Ruby Implementation (react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb):

# Main API
ReactOnRailsPro::LicenseValidator.validated_license_data!  # Returns hash with license data
ReactOnRailsPro::LicenseValidator.evaluation?              # true if free/evaluation license
ReactOnRailsPro::LicenseValidator.grace_days_remaining     # Integer or nil
ReactOnRailsPro::LicenseValidator.reset!                   # Clear cache (testing)

Node.js Implementation (packages/node-renderer/src/shared/licenseValidator.ts):

// Main API
getValidatedLicenseData()    // Returns LicenseData interface
isEvaluation()               // true if free/evaluation license
getGraceDaysRemaining()      // number | undefined
reset()                      // Clear cache (testing)

Grace Period Behavior:

The grace period applies ONLY in production environments and is 1 month for ALL licenses (free and paid):

  • Production Environment (NODE_ENV=production / Rails.env.production?):

    • Grace period: 1 month (30 days) after license expires
    • ⚠️ During grace: Logs ERROR messages on every startup showing days remaining
    • Continues running: Application stays operational during grace period
    • After grace: Application fails to start
  • Non-Production Environments (development, test, CI):

    • No grace period: Application exits immediately when license expires
    • 🎯 Purpose: Helps catch expiration issues early in development/testing

Why this design?

  • Production deployments need buffer time to renew licenses without downtime
  • Development/test environments should fail fast to catch expiration before production
  • Same grace period for all license types keeps behavior predictable

License Sources (checked in order):

  1. Environment variable: REACT_ON_RAILS_PRO_LICENSE
  2. Config file: config/react_on_rails_pro_license.key

JWT Structure:

{
  "sub": "[email protected]",        // Email (REQUIRED)
  "iat": 1234567890,                 // Issued at timestamp (REQUIRED)
  "exp": 1234567890,                 // Expiration timestamp (REQUIRED) - duration set by issuer
  "plan": "free",                    // "free" or "paid" or "paid_*" (Optional)
  "organization": "Your Company",    // Company name (Optional)
  "iss": "api"                       // Issuer (Optional)
}

License Duration:

  • License duration is encoded in the JWT's exp (expiration) field
  • Free evaluation: Typically 3 months (set when JWT is issued)
  • Paid licenses: Duration based on subscription (e.g., 1 year, 2 years) - set when JWT is issued
  • The validation code doesn't hardcode durations - it only checks if exp < current time

Attribution Comments

HTML Comments in Rendered Output:

  • Added to all Rails views using React on Rails
  • Shows version info and license status
  • Example output:
    <!-- React on Rails Pro: Using React on Rails Pro with free evaluation license (expires in 45 days) -->

Implementation:

  • ReactOnRailsPro::Utils.pro_attribution_comment generates comment
  • Integrated into view helpers automatically
  • Shows grace period warnings when applicable

Testing

Ruby Tests (react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb):

  • ✅ Valid license validation
  • ✅ Expired license handling (production grace period vs non-production immediate exit)
  • ✅ Missing required fields
  • ✅ Invalid signature detection
  • ✅ Missing license error
  • ✅ Caching behavior
  • ✅ Environment vs file loading
  • evaluation? method for different plans
  • reset! functionality

Node.js Tests (packages/node-renderer/tests/licenseValidator.test.ts):

  • ✅ Valid license from environment variable
  • ✅ Expired license in non-production (exits immediately)
  • ✅ Missing expiration field
  • ✅ Invalid signature
  • ✅ Missing license
  • ✅ Caching validation results
  • isEvaluation() for free vs paid licenses
  • reset() clears cache

Changes by Component

Ruby Gem (react_on_rails_pro/)

New Files:

  • lib/react_on_rails_pro/license_validator.rb - Main validation logic
  • lib/react_on_rails_pro/license_public_key.rb - RSA public key
  • spec/react_on_rails_pro/license_validator_spec.rb - Comprehensive tests

Modified Files:

  • lib/react_on_rails_pro/utils.rb - Added pro_attribution_comment method
  • react_on_rails_pro.gemspec - Added jwt gem dependency (~> 2.7)
  • .gitignore - Added config/react_on_rails_pro_license.key

Node Renderer (react_on_rails_pro/packages/node-renderer/)

New Files:

  • src/shared/licenseValidator.ts - Validation module (functional pattern)
  • src/shared/licensePublicKey.ts - Public key for Node
  • tests/licenseValidator.test.ts - Jest tests

Modified Files:

  • src/master.ts - Added license validation on startup
  • package.json - Added jsonwebtoken dependency (^9.0.0)

Documentation

New Files:

  • react_on_rails_pro/LICENSE_SETUP.md - Complete license setup guide

    • Getting FREE evaluation licenses
    • Installation methods (ENV var vs config file)
    • Grace period explanation
    • Troubleshooting guide
    • FAQ section
  • react_on_rails_pro/CI_SETUP.md - CI/CD configuration guide

    • Step-by-step for GitHub Actions, GitLab CI, CircleCI, Travis, Jenkins, Bitbucket
    • License renewal process
    • Security best practices

Modified Files:

  • CHANGELOG.md - Added attribution comment feature
  • react_on_rails_pro/CHANGELOG.md - Added license validation system and attribution comment

How It Works

┌─────────────────────────────────────────────────────────────┐
│ 1. License Storage                                           │
│    ├─ ENV['REACT_ON_RAILS_PRO_LICENSE']                     │
│    └─ config/react_on_rails_pro_license.key                 │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Validation Points                                         │
│    ├─ Ruby Gem: Rails startup                               │
│    │   └─ ReactOnRailsPro::LicenseValidator.validated_license_data! │
│    └─ Node Renderer: Node startup                           │
│        └─ getValidatedLicenseData()                         │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Validation Logic                                          │
│    ├─ JWT.verify(token, PUBLIC_KEY, algorithm: 'RS256')     │
│    ├─ Check required fields (sub, iat, exp)                 │
│    ├─ Check expiration:                                     │
│    │   ├─ PRODUCTION: 1-month grace period for any license  │
│    │   └─ NON-PROD: Fail immediately                        │
│    └─ Cache result for performance                          │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Outcomes                                                  │
│    ├─ Valid: App starts, cache license data                 │
│    ├─ Expired in Production:                                │
│    │   ├─ Within grace period (30 days): Continue + ERROR logs │
│    │   └─ After grace period: Exit                          │
│    └─ Expired in Dev/Test/CI: Exit immediately              │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Attribution                                               │
│    └─ HTML comment added to rendered views                  │
│       "React on Rails Pro: [license status]"                │
└─────────────────────────────────────────────────────────────┘

License Types

FREE Evaluation License

  • Duration: Set in JWT exp field (typically 3 months from issue date)
  • Usage: Personal, educational, evaluation purposes (dev/test/CI)
  • NOT for production deployment
  • Get it: https://shakacode.com/react-on-rails-pro
  • Expiration behavior:
    • Development/Test/CI: Fails immediately on expiration
    • Production: 1-month grace period, then fails
  • Renewal: Get new FREE license every time it expires

Paid License

  • Duration: Set in JWT exp field based on subscription (e.g., 1 year, 2 years)
  • Usage: Production deployment
  • Includes: Professional support
  • Expiration behavior:
    • Development/Test/CI: Fails immediately on expiration
    • Production: 1-month grace period, then fails
  • Renewal: Contact ShakaCode before expiration

Note: Grace period behavior is the same for both license types - only the initial duration (encoded in exp) differs.

Security Features

  • Offline validation: No internet or phone-home required
  • RSA-256 signatures: Cannot be forged without private key
  • Tamper-proof: Any modification invalidates signature
  • Algorithm enforcement: Only RS256 accepted (prevents downgrade attacks)
  • No tracking: License validated locally using cryptography
  • Config file in .gitignore: Prevents accidental commits

Breaking Changes

None. This is a new feature for React on Rails Pro.

Migration Guide

For existing React on Rails Pro users:

  1. Get a license (FREE evaluation): https://shakacode.com/react-on-rails-pro
  2. Set environment variable or create config file
  3. That's it! - Your app will validate on startup

See react_on_rails_pro/LICENSE_SETUP.md for detailed instructions.

Test Plan

  • Ruby specs pass (15 examples, 0 failures)
  • Node tests pass (9 examples, 0 failures)
  • ESLint passes with 0 errors
  • RuboCop passes with 0 offenses
  • All git hooks pass
  • License file properly ignored in git
  • Attribution comments appear in rendered HTML
  • Grace period logic tested (production vs non-production)
  • End-to-end testing with real JWT tokens
  • Integration testing in example apps

Files Changed

Added (9 files)

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
  • react_on_rails_pro/LICENSE_SETUP.md
  • react_on_rails_pro/CI_SETUP.md
  • Public key management tooling

Modified (8 files)

  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb - Added attribution comment
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb - License validation integration
  • react_on_rails_pro/packages/node-renderer/src/master.ts - Startup validation
  • react_on_rails_pro/react_on_rails_pro.gemspec - Added jwt dependency
  • react_on_rails_pro/packages/node-renderer/package.json - Added jsonwebtoken
  • react_on_rails_pro/.gitignore - Added license file pattern
  • CHANGELOG.md - Attribution comment entry
  • react_on_rails_pro/CHANGELOG.md - License validation entries

Next Steps

  1. Implementation complete - All validation logic and tests passing
  2. Documentation complete - LICENSE_SETUP.md and CI_SETUP.md written
  3. Replace placeholder keys - Use ShakaCode's actual RSA key pair
  4. Set up license generation - Backend service for issuing licenses
  5. End-to-end testing - Test with real licenses in example apps
  6. Deploy to users - Release with next version

FAQ

Q: Why do I need a license for development?
A: FREE evaluation licenses let us track usage and provide better support. Registration takes 30 seconds!

Q: What happens when my license expires in development?
A: The app fails to start immediately (no grace period in dev/test/CI). This helps you catch expiration early before it affects production.

Q: What happens when my license expires in production?
A: You get a 1-month grace period with ERROR logs showing days remaining. This gives you time to renew without downtime. Monitor your logs!

Q: Is the grace period different for free vs paid licenses?
A: No! Both get the same 1-month grace period in production. The difference is the initial license duration (encoded in the JWT's exp field).

Q: Can I use free licenses in production?
A: Free licenses are intended for evaluation only (dev/test/CI). Production should use paid licenses, but technically both work the same way with the same grace period.

Q: Does validation require internet?
A: No! Everything is validated offline using cryptographic signatures.

Q: How long do licenses last?
A: License duration is set when the JWT is issued:

  • Free evaluation: Typically 3 months
  • Paid: Based on your subscription (e.g., 1 year, 2 years)

🤖 Generated with Claude Code

Co-Authored-By: Claude [email protected]


This change is Reviewable

Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

Walkthrough

Adds a JWT-based Pro license system: public RSA keys (Ruby/TS), validators in Ruby and Node with manual expiry/grace handling and caching, engine and node preflight checks, rake tasks to update public keys, attribution insertion into rendered output, docs and CI guidance, dependency updates, and tests.

Changes

Cohort / File(s) Summary of changes
Docs
react_on_rails_pro/CI_SETUP.md, react_on_rails_pro/LICENSE_SETUP.md
New CI and license setup guides covering acquisition, installation (ENV/file), verification, troubleshooting, and security guidance.
Public key artifacts & rake tasks
react_on_rails_pro/lib/.../license_public_key.rb, react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts, react_on_rails_pro/rakelib/public_key_management.rake
Added PEM public key constants for Ruby and Node and rake tasks to fetch/rotate/emit those public key artifacts.
Ruby: License validator & wiring
react_on_rails_pro/lib/.../license_validator.rb, react_on_rails_pro/lib/react_on_rails_pro.rb, react_on_rails_pro/lib/react_on_rails_pro/engine.rb, react_on_rails_pro/lib/react_on_rails_pro/utils.rb
New LicenseValidator implementing RS256 JWT decode (ignoreExpiration), manual exp/grace (30d) validation, caching, APIs (validated_license_data!, evaluation?, grace_days_remaining, reset!); engine initializer invokes validation at boot; utils delegate and provide pro_attribution_comment.
Node: License validator & preflight
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts, react_on_rails_pro/packages/node-renderer/src/master.ts, react_on_rails_pro/package.json
New Node validator (uses jsonwebtoken, RS256, ignoreExpiration), cached validation with process exit on invalid; masterRun preflight calls getValidatedLicenseData(); added jsonwebtoken and @types/jsonwebtoken.
Ruby: Integrations & helpers
lib/react_on_rails/pro_utils.rb, lib/react_on_rails/utils.rb, lib/react_on_rails/helper.rb
Switched Pro gating to react_on_rails_pro? that checks gem + validated license data; removed old predicate; added react_on_rails_attribution_comment and integrated attribution into rails context output.
Tests
react_on_rails_pro/spec/.../license_validator_spec.rb, react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts, various spec updates under spec/dummy/ and spec/react_on_rails/
Added comprehensive Ruby and Node tests for license validator scenarios; updated many tests to use react_on_rails_pro? and added attribution-related specs.
Packaging & ignores
react_on_rails_pro/react_on_rails_pro.gemspec, react_on_rails_pro/.gitignore, react_on_rails_pro/spec/dummy/.gitignore, react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore
Added runtime jwt gem dependency, adjusted gemspec exclusions, and ignored config/react_on_rails_pro_license.key in .gitignore files.
Changelogs
CHANGELOG.md, react_on_rails_pro/CHANGELOG.md
Documented the attribution feature and newly introduced License Validation System.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Rails as Rails App
  participant Engine as ReactOnRailsPro::Engine
  participant Validator as ReactOnRailsPro::LicenseValidator

  App->>Rails: boot
  Rails->>Engine: after_initialize
  Engine->>Validator: validated_license_data!()
  alt valid
    Validator-->>Engine: license data (cached)
    Engine-->>Rails: log validation success
  else invalid
    Validator-->>Engine: raises ReactOnRailsPro::Error
    Engine-->>Rails: log error / abort
  end
Loading
sequenceDiagram
  autonumber
  actor CLI
  participant Master as node-renderer/master.ts
  participant LV as Node LicenseValidator
  participant ENV as Env
  participant FS as FileSystem

  CLI->>Master: masterRun()
  Master->>LV: getValidatedLicenseData()
  LV->>ENV: read REACT_ON_RAILS_PRO_LICENSE
  alt license in ENV
    LV-->>LV: decode & verify JWT (RS256, PUBLIC_KEY)
  else not in ENV
    LV->>FS: read `config/react_on_rails_pro_license.key`
    LV-->>LV: decode & verify JWT (RS256, PUBLIC_KEY)
  end
  LV->>LV: check exp and production grace window
  alt valid / within grace
    LV-->>Master: success (cached)
    Master-->>CLI: continue startup
  else invalid
    LV-->>Master: log error and process.exit(1)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • justin808
  • Judahmeek
  • alexeyr-ci2

Poem

In my burrow I guard a PEM so bright,
I hum JWTs through day and night.
Rails boots, Node hops a validation beat,
Keys in place — startup's rhythm is neat.
Thump! says the rabbit, licence checked complete. 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and succinctly summarizes the primary change in this pull request by stating that an offline JWT-based license validation system is being added for React on Rails Pro, directly reflecting the implemented functionality without extraneous detail. It is concise, specific, and immediately communicates the core update a reviewer needs to understand at a glance. The phrasing highlights the main feature and aligns with the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 feature/pro-license-validation

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.

❤️ Share

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

Copy link

claude bot commented Oct 8, 2025

Code Review: Offline JWT-based License Validation System

Overall Assessment

This is a well-structured implementation of an offline license validation system. The code is clean, well-tested, and follows good security practices. However, there are several important considerations before merging.

Critical Issues

1. Placeholder Public Keys in Production Code (BLOCKER)

Files: license_public_key.rb:8-17, licensePublicKey.ts:4-10

The PR includes placeholder RSA public keys with comments indicating they should be replaced. This is a critical security risk if merged as-is.

Recommendations:

  • Replace with actual ShakaCode public keys before merging
  • Add CI checks to prevent placeholder keys from being deployed
  • Consider adding a startup warning if placeholder keys are detected

2. Potential Race Condition in Ruby Validator (MEDIUM)

File: license_validator.rb:9-13

The defined? check is not thread-safe. If multiple threads call valid? simultaneously, they could all trigger validation concurrently.

Recommendation: Add mutex synchronization for thread safety

3. File I/O on Every Failed Validation (MEDIUM)

File: licenseValidator.ts:109-111

Failed reads will attempt file I/O on every Node renderer startup.

Recommendation: Cache the license string itself, not just the validation result

4. JWT Library Version Pinning (MEDIUM)

Files: package.json:47, react_on_rails_pro.gemspec:35

The JWT libraries use caret versioning which allows minor version updates that could introduce breaking changes.

Recommendation: Pin to exact versions for security-critical dependencies

Strengths

Code Quality

  • Excellent code organization with clear separation between Ruby and TypeScript
  • Consistent error handling and proper distinction between dev and production
  • Good singleton pattern usage with caching
  • Clean separation of concerns

Security

  • Strong RSA-256 JWT signatures (industry-standard)
  • Proper algorithm specification (prevents algorithm confusion attacks)
  • Environment-aware behavior
  • License file properly excluded from version control

Testing

  • Comprehensive test coverage in both Ruby (179 lines) and TypeScript (249 lines)
  • Real cryptographic testing using actual RSA key pairs
  • Good edge case coverage (expired, invalid, missing licenses)

Test Coverage Assessment

Both Ruby and TypeScript tests are excellent with comprehensive coverage of:

  • Valid license scenarios
  • Expired license handling
  • Invalid signatures
  • Missing licenses
  • Development vs production behavior
  • Caching behavior
  • File vs ENV loading

Recommendation: Add integration tests for:

  • License validation during actual Rails request
  • Node renderer startup/shutdown with invalid licenses
  • Browser railsContext.rorPro value propagation

Security Considerations

Good:

  • Offline validation prevents MITM attacks
  • RSA-256 signatures cannot be forged
  • Algorithm explicitly specified
  • No sensitive data in logs (except dev)

Areas for improvement:

  • Consider adding issuer (iss) validation to JWT claims
  • Consider adding not-before (nbf) claim support
  • Document file permissions (should be 0600)
  • Consider rate limiting validation attempts

Performance

Good practices:

  • Validation results are cached (singleton pattern)
  • JWT verification only runs once per process
  • No network calls required

Potential improvements:

  • Consider lazy loading of JWT library
  • Better caching strategy for file I/O
  • Pre-validate during deployment/build

Documentation

Strengths:

  • Comprehensive LICENSE_SETUP.md
  • Good inline comments
  • Helpful error messages

Suggestions:

  • Add security section about file permissions
  • Document JWT payload structure
  • Add crypto error troubleshooting
  • Document license rotation process

Code Style

Ruby:

  • Follows RuboCop conventions (per CLAUDE.md)
  • Proper use of frozen string literals
  • Good error inheritance

TypeScript:

  • Proper use of interfaces
  • Good singleton pattern
  • Appropriate optional chaining

Checklist Before Merge

  • Replace placeholder RSA public keys with actual ShakaCode keys
  • Add thread safety to Ruby validator
  • Pin JWT library versions to exact versions
  • Add input validation for JWT format
  • Add CI check to detect placeholder keys
  • Add integration tests for license propagation
  • Document file permissions requirements
  • Consider adding issuer validation
  • Test manual license updates in running systems
  • Verify behavior in air-gapped environments

Recommendation

Conditional Approval - This is high-quality code that is well-architected and thoroughly tested. However, the placeholder public keys are a blocker that must be resolved before merge.

The implementation demonstrates strong understanding of JWT security, proper error handling, and good testing practices. The dual Ruby/TypeScript architecture is well-designed and maintainable.

Great work! 🚀

Copy link
Contributor

@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: 6

🧹 Nitpick comments (15)
react_on_rails_pro/LICENSE_SETUP.md (2)

71-71: Add language identifier to the fenced code block.

The code block is missing a language specifier, which affects rendering and syntax highlighting.

Apply this diff:

-```
+```text
 [React on Rails Pro] License validation successful

As per coding guidelines (markdownlint)

---

`128-129`: **Format bare URLs properly.**

The bare URLs should be properly formatted as links or enclosed in angle brackets for better markdown rendering.

Apply this diff:

```diff
-3. Contact ShakaCode support at [email protected]
-4. Visit https://shakacode.com/react-on-rails-pro for license management
+3. Contact ShakaCode support at <[email protected]>
+4. Visit <https://shakacode.com/react-on-rails-pro> for license management

As per coding guidelines (markdownlint)

react_on_rails_pro/react_on_rails_pro.gemspec (1)

35-35: Consider bumping the JWT dependency to ~> 3.0
Latest stable jwt is 3.1.2; updating to ~> 3.0 will allow 3.x security and bug fixes.

lib/react_on_rails/helper.rb (1)

380-390: DRY: cache licence validity in a local var within rails_context

You call ReactOnRails::Utils.react_on_rails_pro_licence_valid? twice. It’s memoized, but caching locally improves readability and avoids repeated calls.

Apply this diff:

-          # TODO: v13 just use the version if existing
-          rorPro: ReactOnRails::Utils.react_on_rails_pro_licence_valid?
+          # TODO: v13 just use the version if existing
+          rorPro: (lic_valid = ReactOnRails::Utils.react_on_rails_pro_licence_valid?)
         }
 
-        if ReactOnRails::Utils.react_on_rails_pro_licence_valid?
+        if lic_valid
           result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version

Note: No behavior change. Based on learnings.

react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (2)

5-9: Redundant licensePublicKey mocks

You mock licensePublicKey at the top and again in beforeEach. Keep only the beforeEach doMock to inject the per-test key and remove the top-level mock to reduce confusion.

Apply this diff:

-// Mock modules
-jest.mock('fs');
-jest.mock('../src/shared/licensePublicKey', () => ({
-  PUBLIC_KEY: ''
-}));
+// Mock modules
+jest.mock('fs');

Also applies to: 36-39


52-55: Restore NODE_ENV after tests to avoid cross-test bleed

Tests mutate process.env.NODE_ENV but never restore it, risking order-dependent failures.

Apply this diff:

   beforeEach(() => {
+    originalNodeEnv = process.env.NODE_ENV;
     // Clear the module cache to get a fresh instance
     jest.resetModules();
@@
   afterEach(() => {
     delete process.env.REACT_ON_RAILS_PRO_LICENSE;
     jest.restoreAllMocks();
+    if (originalNodeEnv === undefined) {
+      delete process.env.NODE_ENV;
+    } else {
+      process.env.NODE_ENV = originalNodeEnv;
+    }
   });

And add the variable at the top of the describe:

 describe('LicenseValidator', () => {
   let licenseValidator: any;
   let testPrivateKey: string;
   let testPublicKey: string;
+  let originalNodeEnv: string | undefined;

Also applies to: 81-82, 101-102, 133-134, 146-147

react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1)

40-47: Harden JWT claim validation and add clock skew tolerance

Current logic only checks exp manually. Consider:

  • Add a small leeway for clock skew to reduce false negatives.
  • Optionally validate issuer/audience if you rely on them.

Two incremental options:

  • Minimal: add leeway to the manual expiry check:
-          if license["exp"] && Time.now.to_i > license["exp"]
+          leeway = 60 # seconds
+          if license["exp"] && Time.now.to_i > (license["exp"] + leeway)
             @validation_error = "License has expired"
  • Library-based: delegate exp check to JWT with leeway (keeps your messaging):
-        JWT.decode(
-          license_string,
-          public_key,
-          true,
-          algorithm: "RS256"
-        ).first
+        JWT.decode(
+          license_string,
+          public_key,
+          true,
+          algorithm: "RS256",
+          leeway: 60
+        ).first

If you later introduce iss/aud, also pass verify_iss/verify_aud and expected values. As per coding guidelines.

Also applies to: 62-68, 90-100

react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)

107-112: Spec description vs behavior mismatch

Description says “returns false in production with error” but the expectation asserts a raised error. Consider renaming for clarity.

-      it "returns false in production with error" do
+      it "raises error in production when no license found" do
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (7)

1-4: Use JwtPayload for safer typing instead of any-based custom interface

Leverage jsonwebtoken’s JwtPayload to avoid any and align with standard claims.

-import * as jwt from 'jsonwebtoken';
+import * as jwt from 'jsonwebtoken';
+import type { JwtPayload } from 'jsonwebtoken';
@@
-interface LicenseData {
-  sub?: string;
-  iat?: number;
-  exp?: number;
-  [key: string]: any;
-}
+type LicenseData = JwtPayload & Record<string, unknown>;

Also applies to: 6-12


63-69: Drop redundant manual exp check; handle TokenExpiredError explicitly

verify() already enforces exp; simplify and improve error classification.

-      // Check expiry if present
-      if (license.exp && Date.now() / 1000 > license.exp) {
-        this.validationError = 'License has expired';
-        this.handleInvalidLicense(isDevelopment, this.validationError);
-        return isDevelopment;
-      }
+      // Additional claim validations can be added here if needed (e.g., custom fields)
@@
-    } catch (error: any) {
-      if (error.name === 'JsonWebTokenError') {
-        this.validationError = `Invalid license signature: ${error.message}`;
-      } else {
-        this.validationError = `License validation error: ${error.message}`;
-      }
+    } catch (error: unknown) {
+      if (error instanceof jwt.TokenExpiredError) {
+        this.validationError = 'License has expired';
+      } else if (error instanceof jwt.JsonWebTokenError) {
+        this.validationError = `Invalid license signature: ${(error as Error).message}`;
+      } else {
+        this.validationError = `License validation error: ${(error as Error).message}`;
+      }
       this.handleInvalidLicense(isDevelopment, this.validationError);
       return isDevelopment;
     }

Also applies to: 71-79


88-98: Simplify decode path and add small clock tolerance

Remove no-op try/catch and allow a few seconds of skew.

-    try {
-      const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
-        algorithms: ['RS256']
-      }) as LicenseData;
-
-      this.licenseData = decoded;
-      return decoded;
-    } catch (error) {
-      throw error;
-    }
+    const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
+      algorithms: ['RS256'],
+      clockTolerance: 5,
+    }) as LicenseData;
+    this.licenseData = decoded;
+    return decoded;

88-91: Consider enforcing issuer/audience claims in verify()

Strengthen verification by validating iss/aud once you finalize them (keeps tokens scoped and prevents key misuse).

What issuer/audience do you plan to set on license JWTs? Once decided, pass them to verify(), e.g.:

const verifyOptions: jwt.VerifyOptions = {
  algorithms: ['RS256'],
  clockTolerance: 5,
  issuer: process.env.REACT_ON_RAILS_PRO_ISSUER, // or a constant
  audience: 'react-on-rails-pro',
};
jwt.verify(licenseString, PUBLIC_KEY, verifyOptions);

101-106: Trim ENV value to avoid whitespace-induced failures

Protect against accidental newlines/spaces in env vars.

-    const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE;
+    const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE?.trim();
     if (envLicense) {
       return envLicense;
     }

107-116: Avoid existsSync TOCTOU; read and catch directly

Simpler and race-free.

-    try {
-      const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
-      if (fs.existsSync(configPath)) {
-        return fs.readFileSync(configPath, 'utf8').trim();
-      }
-    } catch (error) {
-      // File doesn't exist or can't be read
-    }
+    try {
+      const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
+      return fs.readFileSync(configPath, 'utf8').trim();
+    } catch {
+      // File doesn't exist or can't be read
+    }

4-4: Confirm RSA key strength and rotation plan

Ensure PUBLIC_KEY is RSA ≥ 2048 bits and document rotation.

Run locally:

  • Check key size: openssl pkey -pubin -in <(printf "%s" "$PUBLIC_KEY") -text -noout | rg 'Public-Key'
  • Plan: version keys with kid and support rotation by selecting key via header.kid (future-proof for multiple public keys).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80eb21e and b98f261.

📒 Files selected for processing (14)
  • .gitignore (1 hunks)
  • lib/react_on_rails/helper.rb (1 hunks)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1 hunks)
  • react_on_rails_pro/package.json (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/master.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1 hunks)
  • react_on_rails_pro/react_on_rails_pro.gemspec (1 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb
  • react_on_rails_pro/react_on_rails_pro.gemspec
  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
  • lib/react_on_rails/helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts
  • react_on_rails_pro/package.json
  • react_on_rails_pro/LICENSE_SETUP.md
  • react_on_rails_pro/packages/node-renderer/src/master.ts
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts
  • react_on_rails_pro/packages/node-renderer/src/master.ts
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • lib/react_on_rails/helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • lib/react_on_rails/helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
🧬 Code graph analysis (7)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)
  • valid? (7-102)
  • valid? (9-13)
react_on_rails_pro/lib/react_on_rails_pro.rb (1)
react_on_rails_pro/spec/dummy/client/node-renderer.js (1)
  • require (8-8)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (6)
  • public_key (86-88)
  • reset! (15-19)
  • valid? (7-102)
  • valid? (9-13)
  • license_data (21-23)
  • validation_error (25-27)
lib/react_on_rails/helper.rb (1)
lib/react_on_rails/utils.rb (1)
  • react_on_rails_pro_licence_valid? (249-262)
react_on_rails_pro/packages/node-renderer/src/master.ts (2)
react_on_rails_pro/packages/node-renderer/src/shared/configBuilder.ts (1)
  • Config (25-83)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (2)
  • isLicenseValid (142-144)
  • getLicenseValidationError (150-152)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (4-11)
react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
  • licenseValidator (140-140)
🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/LICENSE_SETUP.md

71-71: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


128-128: Bare URL used

(MD034, no-bare-urls)


129-129: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: claude-review
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: build
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: build-and-test
🔇 Additional comments (5)
.gitignore (1)

78-80: LGTM!

The addition of the license key file to .gitignore is correct and follows security best practices by preventing sensitive license information from being committed to version control.

react_on_rails_pro/package.json (1)

47-47: LGTM!

The jsonwebtoken dependency version ^9.0.2 is appropriate. This is the latest stable version (as of August 2023) and includes important security fixes from the v9 series, including hardened key validation, stricter algorithm enforcement, and fixes for multiple CVEs.

Based on learnings

react_on_rails_pro/lib/react_on_rails_pro.rb (1)

12-13: LGTM!

The placement of the license module requires is appropriate. Loading them early in the initialization sequence ensures license validation is available before any Pro features that depend on it.

react_on_rails_pro/packages/node-renderer/src/master.ts (1)

15-29: License validation logic is well-implemented.

The pre-flight license check correctly gates the startup sequence:

  • Validates before configuration and worker initialization
  • Handles development vs. production environments appropriately
  • Provides clear logging for all scenarios

The hard exit in production (process.exit(1)) is appropriate for license validation failures.

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)

128-137: No action needed: production exit for invalid license is correctly wired

Comment on lines 1 to 8
# frozen_string_literal: true

module ReactOnRailsPro
module LicensePublicKey
# This is a placeholder public key for development/testing
# In production, this should be replaced with ShakaCode's actual public key
# The private key corresponding to this public key should NEVER be committed to the repository
KEY = OpenSSL::PKey::RSA.new(<<~PEM)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Missing require 'openssl' causes load-time NameError

OpenSSL is used but not required. This can raise NameError when the file loads (especially since this file is required before jwt).

Apply this diff:

 # frozen_string_literal: true
 
+require "openssl"
+
 module ReactOnRailsPro
   module LicensePublicKey

Also, ensure the placeholder key is replaced before release and stays in sync with the Node PUBLIC_KEY.


🏁 Script executed:

#!/bin/bash
rg -n "require.*openssl" -C2 react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb

Length of output: 0


🏁 Script executed:

#!/bin/bash
sed -n '1,20p' react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb

Length of output: 874


Add missing require "openssl" to avoid NameError

This file invokes OpenSSL::PKey without requiring it, which can raise a NameError at load time.
Apply:

 # frozen_string_literal: true
 
+require "openssl"
 
 module ReactOnRailsPro
   module LicensePublicKey

Ensure the placeholder public key is replaced in production and kept in sync with the Node PUBLIC_KEY.

📝 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
# frozen_string_literal: true
module ReactOnRailsPro
module LicensePublicKey
# This is a placeholder public key for development/testing
# In production, this should be replaced with ShakaCode's actual public key
# The private key corresponding to this public key should NEVER be committed to the repository
KEY = OpenSSL::PKey::RSA.new(<<~PEM)
# frozen_string_literal: true
require "openssl"
module ReactOnRailsPro
module LicensePublicKey
# This is a placeholder public key for development/testing
# In production, this should be replaced with ShakaCode's actual public key
# The private key corresponding to this public key should NEVER be committed to the repository
KEY = OpenSSL::PKey::RSA.new(<<~PEM)
🤖 Prompt for AI Agents
In react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb lines 1-8,
the file uses OpenSSL::PKey::RSA but does not require "openssl", which can raise
a NameError at load time; add require "openssl" at the top of the file before
referencing OpenSSL, and verify the placeholder public key remains for
dev/testing only and is replaced in production and kept in sync with the Node
PUBLIC_KEY.

Comment on lines 19 to 21
def self.licence_valid?
LicenseValidator.valid?
end
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consider spelling consistency for the method name.

The method is named licence_valid? (British spelling) while the rest of the codebase uses American spelling "license" (e.g., LicenseValidator, license_public_key, etc.). This inconsistency could cause confusion for developers.

Consider renaming to license_valid? for consistency:

-def self.licence_valid?
+def self.license_valid?
   LicenseValidator.valid?
 end

Note: If you make this change, also update:

  • Documentation in LICENSE_SETUP.md (line 63)
  • Any other references in the codebase
  • Tests that may reference this method
📝 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
def self.licence_valid?
LicenseValidator.valid?
end
def self.license_valid?
LicenseValidator.valid?
end
🤖 Prompt for AI Agents
In react_on_rails_pro/lib/react_on_rails_pro/utils.rb around lines 19 to 21, the
method is named `licence_valid?` using British spelling which is inconsistent
with the rest of the codebase that uses American spelling (`LicenseValidator`,
`license_public_key`, etc.); rename the method to `license_valid?` and update
all call sites, tests, and documentation (specifically LICENSE_SETUP.md line 63)
to the new name to maintain consistency and ensure nothing breaks.

Comment on lines 63 to 64
> ReactOnRails::Utils.react_on_rails_pro_licence_valid?
# Should return true if license is valid
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the method name matches the implementation.

The documentation references ReactOnRails::Utils.react_on_rails_pro_licence_valid?, but based on the implementation in utils.rb (line 19), the method is licence_valid? (not react_on_rails_pro_licence_valid?). Ensure this example accurately reflects the actual API.


🏁 Script executed:

#!/bin/bash
# Verify the correct method name in the implementation
rg -n "def (self\.)?.*licence.*valid" react_on_rails_pro/lib/react_on_rails_pro/utils.rb

Length of output: 30


Use the correct method in docs: replace ReactOnRails::Utils.react_on_rails_pro_licence_valid? with ReactOnRailsPro::Utils.licence_valid?.

🤖 Prompt for AI Agents
In react_on_rails_pro/LICENSE_SETUP.md around lines 63 to 64, the documented
method name is incorrect; replace the reference to
ReactOnRails::Utils.react_on_rails_pro_licence_valid? with the correct API
ReactOnRailsPro::Utils.licence_valid? so the docs call the right module and
method name; update the text accordingly to reflect the exact namespace and
method.

Comment on lines 1 to 11
// This is a placeholder public key for development/testing
// In production, this should be replaced with ShakaCode's actual public key
// The private key corresponding to this public key should NEVER be committed to the repository
export const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF/NP
lRQkqfph3x6TEOirFCpDfRjowDXAk66dPmLzw5qVOmGVPKgpJBjZR7oMIMgxBPUoj00F
DwlhUGmOVoqnVWGFHVUHDL5qYQaZzRdp4Bh9fxnN52Yk8+FuHsT+5lxLcaRV6mRtX7OT
5pQbxV0o0/OxPFC1Hz9RdLPUevnWNbLe8f5ePHivmqsoAH9HE4g03WkFZEqBLmjqpJj8
VqGR0q8CPPRCFGAr9S4WCQqBhLDH0j/JR+FpPX9Df8vfFJhHdBGdTGjN4g9g6qwPYmVH
ukAErHNIJMNmzYjFIT4+Xwp6xKHyUqL3w3JZDQnFywIDAQAB
-----END PUBLIC KEY-----`;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

❓ Verification inconclusive

CRITICAL: Ensure placeholder key is replaced before production deployment.

The placeholder public key is appropriately marked for development/testing. However, deploying with this key in production would create a critical security vulnerability, as anyone could generate valid licenses using the corresponding private key.

Recommendations:

  1. Add a runtime check to detect placeholder keys in production:
// At the top of licensePublicKey.ts or in licenseValidator.ts
const PLACEHOLDER_KEY_MARKER = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn';

if (process.env.NODE_ENV === 'production' && PUBLIC_KEY.includes(PLACEHOLDER_KEY_MARKER)) {
  throw new Error(
    'SECURITY ERROR: Placeholder license key detected in production. ' +
    'Replace with production key from ShakaCode before deploying.'
  );
}
  1. Verify the production key meets security requirements:
    • RSA key size >= 2048 bits (4096 bits recommended)
    • Algorithm: RS256

Based on learnings

  1. Document the key replacement process in the deployment checklist.

Do you want me to generate a verification script to check if placeholder keys are still in use, or open an issue to track this critical task?


🏁 Script executed:

#!/bin/bash
cat << 'EOF' > tmp_public_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF/NP
lRQkqfph3x6TEOirFCpDfRjowDXAk66dPmLzw5qVOmGVPKgpJBjZR7oMIMgxBPUoj00F
DwlhUGmOVoqnVWGFHVUHDL5qYQaZzRdp4Bh9fxnN52Yk8+FuHsT+5lxLcaRV6mRtX7OT
5pQbxV0o0/OxPFC1Hz9RdLPUevnWNbLe8f5ePHivmqsoAH9HE4g03WkFZEqBLmjqpJj8
VqGR0q8CPPRCFGAr9S4WCQqBhLDH0j/JR+FpPX9Df8vfFJhHdBGdTGjN4g9g6qwPYmVH
ukAErHNIJMNmzYjFIT4+Xwp6xKHyUqL3w3JZDQnFywIDAQAB
-----END PUBLIC KEY-----
EOF

openssl pkey -pubin -in tmp_public_key.pem -text -noout | grep "Public-Key"

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Write the placeholder key to a temp file
cat << 'EOF' > tmp_public_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF/NP
lRQkqfph3x6TEOirFCpDfRjowDXAk66dPmLzw5qVOmGVPKgpJBjZR7oMIMgxBPUoj00F
DwlhUGmOVoqnVWGFHVUHDL5qYQaZzRdp4Bh9fxnN52Yk8+FuHsT+5lxLcaRV6mRtX7OT
5pQbxV0o0/OxPFC1Hz9RdLPUevnWNbLe8f5ePHivmqsoAH9HE4g03WkFZEqBLmjqpJj8
VqGR0q8CPPRCFGAr9S4WCQqBhLDH0j/JR+FpPX9Df8vfFJhHdBGdTGjN4g9g6qwPYmVH
ukAErHNIJMNmzYjFIT4+Xwp6xKHyUqL3w3JZDQnFywIDAQAB
-----END PUBLIC KEY-----
EOF

# Install cryptography library
pip install cryptography --quiet

# Use Python to load the key and print its size
python3 - << 'EOF'
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.backends import default_backend

with open('tmp_public_key.pem', 'rb') as f:
    data = f.read()

key = load_pem_public_key(data, backend=default_backend())
print("Key size:", key.key_size)
EOF

Length of output: 365


🏁 Script executed:

#!/bin/bash
# Re-create the placeholder key file
cat << 'EOF' > tmp_public_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF/NP
lRQkqfph3x6TEOirFCpDfRjowDXAk66dPmLzw5qVOmGVPKgpJBjZR7oMIMgxBPUoj00F
DwlhUGmOVoqnVWGFHVUHDL5qYQaZzRdp4Bh9fxnN52Yk8+FuHsT+5lxLcaRV6mRtX7OT
5pQbxV0o0/OxPFC1Hz9RdLPUevnWNbLe8f5ePHivmqsoAH9HE4g03WkFZEqBLmjqpJj8
VqGR0q8CPPRCFGAr9S4WCQqBhLDH0j/JR+FpPX9Df8vfFJhHdBGdTGjN4g9g6qwPYmVH
ukAErHNIJMNmzYjFIT4+Xwp6xKHyUqL3w3JZDQnFywIDAQAB
-----END PUBLIC KEY-----
EOF

# Install PyCryptodome
pip install pycryptodome --quiet

# Use PyCryptodome to load the PEM and print key size
python3 - << 'EOF'
from Crypto.PublicKey import RSA
import re, base64

with open('tmp_public_key.pem') as f:
    pem = f.read()

# Extract base64 content between the PEM markers
b64 = ''.join(re.findall(r'^[A-Za-z0-9+/=]+$', pem, re.MULTILINE))
data = base64.b64decode(b64)

key = RSA.import_key(data)
print("Key size:", key.size_in_bits())
EOF

Length of output: 495


CRITICAL: Placeholder public key must not reach production

Releasing with this dev key enables license forgery. In react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts:

  • Add a runtime guard to throw if NODE_ENV=production and PUBLIC_KEY matches the placeholder.
  • Replace with ShakaCode’s official RSA ≥2048-bit public key before deployment and document the steps.
🤖 Prompt for AI Agents
In react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts
lines 1-11, the file currently exports a development placeholder public key
which must not be used in production; add a runtime guard that checks if
process.env.NODE_ENV === 'production' and PUBLIC_KEY equals the placeholder
string and, if so, immediately throw a clear error preventing startup; replace
the placeholder with ShakaCode’s official RSA ≥2048-bit public key before
deployment and add a short developer-only comment or README note describing the
replacement steps and where to source/store the official key (e.g., secure
secret manager or env/CI variable).

Copy link
Contributor

@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: 0

🧹 Nitpick comments (5)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)

4-4: Remove unused require.

The pathname library is required but never used in this file.

Apply this diff to remove the unused require:

-require "pathname"

80-94: Consider handling file read errors.

While config_path.exist? checks for file existence, File.read(config_path) at line 87 could still raise an error if the file is not readable due to permissions. Consider wrapping it in a rescue block to provide a clearer error message.

Apply this diff to handle read errors:

 # Then try config file
 config_path = Rails.root.join("config", "react_on_rails_pro_license.key")
-return File.read(config_path).strip if config_path.exist?
+if config_path.exist?
+  begin
+    return File.read(config_path).strip
+  rescue => e
+    @validation_error = "License file exists but cannot be read: #{e.message}"
+    handle_invalid_license(Rails.env.development? || Rails.env.test?, @validation_error)
+    return nil
+  end
+end
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (3)

37-42: Consider error handling in getLicenseData().

The getLicenseData() method calls loadAndDecodeLicense() which can throw errors (lines 102-104), but doesn't catch them. This differs from isValid() which catches errors in validateLicense(). Consider whether callers expect this method to throw or return undefined on error.

If you want consistent error handling, apply this diff:

 public getLicenseData(): LicenseData | undefined {
   if (!this.licenseData) {
-    this.loadAndDecodeLicense();
+    try {
+      this.loadAndDecodeLicense();
+    } catch (error) {
+      // License data remains undefined if loading fails
+    }
   }
   return this.licenseData;
 }

89-105: Simplify error handling.

The verification correctly uses an explicit algorithms: ['RS256'] array, which is a best practice per the jsonwebtoken 9.x security guidelines. However, the catch block at lines 102-104 immediately re-throws the error without modification, making it redundant.

Based on learnings

Apply this diff to remove redundant error handling:

 private loadAndDecodeLicense(): LicenseData | undefined {
   const licenseString = this.loadLicenseString();
   if (!licenseString) {
     return undefined;
   }

-  try {
-    const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
-      algorithms: ['RS256']
-    }) as LicenseData;
+  const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
+    algorithms: ['RS256']
+  }) as LicenseData;

-    this.licenseData = decoded;
-    return decoded;
-  } catch (error) {
-    throw error;
-  }
+  this.licenseData = decoded;
+  return decoded;
 }

107-133: Consider logging file read errors.

The catch block at lines 120-122 silently swallows all errors with only a comment. While this prevents crashes, it could hide real issues like permission errors or filesystem problems. Consider logging these errors in development mode for easier debugging.

Apply this diff to log file read errors in development:

     } catch (error) {
-      // File doesn't exist or can't be read
+      if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
+        console.warn('[React on Rails Pro] Failed to read license file:', error);
+      }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06882ec and 97ece1e.

📒 Files selected for processing (2)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
🧠 Learnings (1)
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
🧬 Code graph analysis (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (2)
react_on_rails_pro/spec/dummy/client/node-renderer.js (1)
  • process (5-5)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (6-14)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: build
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
  • GitHub Check: build-and-test
🔇 Additional comments (9)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (5)

15-19: LGTM!

The reset! method correctly clears all memoized state using proper Ruby idioms with defined? checks.


21-27: LGTM!

The accessor methods follow standard Ruby patterns with appropriate memoization for license_data.


65-78: LGTM! Excellent security practices.

The JWT verification correctly:

  • Hardcodes the RS256 algorithm to prevent algorithm substitution attacks
  • Includes a helpful security comment referencing the Auth0 vulnerability
  • Uses .first to extract the payload from the decode result

96-110: LGTM!

The error handling appropriately:

  • Delegates to a centralized public key
  • Differentiates between development and production environments
  • Provides clear, actionable error messages with visual distinction via ANSI colors
  • Raises an appropriate error in production

31-63: Catching JWT::DecodeError only in validate_license is correct
load_and_decode_license doesn’t rescue JWT errors, so handling them in validate_license is necessary and not duplicated.

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (4)

1-11: LGTM!

The imports and interface definition are correct. The LicenseData interface properly marks exp as required, which aligns with the validation logic.


13-26: LGTM!

The singleton pattern is correctly implemented with a private constructor and lazy initialization, mirroring the Ruby implementation's class-level singleton approach.


54-87: LGTM!

The validation logic correctly:

  • Detects development vs production environments
  • Uses Date.now() / 1000 to compare with Unix timestamp expiry
  • Distinguishes between JWT errors and general errors via error.name
  • Implements environment-aware validation (warns in dev, blocks in prod)

Based on learnings


135-159: LGTM!

The error handling and exports are well-designed:

  • Environment-appropriate console output with ANSI colors matching the Ruby implementation
  • Clean public API via singleton instance and convenience functions
  • Matches Ruby API surface for consistency across languages

Copy link

claude bot commented Oct 12, 2025

PR Review: JWT-based License Validation System

Overview

This PR implements a well-designed offline JWT-based license validation system for React on Rails Pro. The implementation demonstrates good security practices and comprehensive error handling.

✅ Strengths

1. Excellent Security Design

  • Correct JWT algorithm enforcement: Both Ruby and TypeScript properly specify RS256 algorithm, preventing algorithm substitution attacks
  • Good security comment: The note at license_validator.rb:84-86 correctly warns against removing algorithm validation
  • RSA-2048 keys: Appropriate key size for this use case
  • Offline validation: No phone-home behavior, respects privacy

2. Comprehensive Error Handling

  • Clear, actionable error messages that guide users to get FREE licenses
  • Proper error categorization (expired, missing, invalid signature, missing exp field)
  • Helpful error messages include the ShakaCode URL for license acquisition

3. Well-Structured Testing

  • Ruby specs: Excellent coverage with 194 lines of tests
  • TypeScript tests: Comprehensive with 262 lines
  • Both test suites cover all major scenarios
  • Good use of test key pairs to avoid hardcoding secrets

4. Good Documentation

  • LICENSE_SETUP.md: Clear setup instructions
  • CI_SETUP.md: Comprehensive CI/CD configuration guide (491 lines!)
  • Helpful inline comments in code
  • Public key files include source URLs and timestamps

5. Developer Experience

  • Dual loading mechanism (ENV var or config file) provides flexibility
  • Caching prevents repeated validation
  • reset! methods for testing
  • Rake tasks for public key management

🔍 Issues and Recommendations

Critical Issues (Must Fix Before Merge)

1. Inconsistent Error Behavior Between Ruby and Node

The Node licenseValidator.ts:handleInvalidLicense() calls process.exit(1) unconditionally, making the development mode check in master.ts:14-28 unreachable when validation fails.

Recommendation: Make handleInvalidLicense() not exit, just set error state, and let the caller decide based on environment.

2. Missing Newlines at End of Files

Per CLAUDE.md requirements, all files must end with a newline. Run bundle exec rubocop before pushing.

3. Placeholder Public Keys

The public keys show Source: http://localhost:8788/api/public-key - these need to be replaced with production keys before merge.

4. RuboCop Compliance Required

You MUST run bundle exec rubocop and fix ALL violations before pushing per CLAUDE.md requirements.

Security Recommendations

5. Add JWT Format Validation

Add basic JWT format validation before decoding to prevent confusing errors when users set malformed values.

Code Quality Issues

6. Redundant Exception Handling

The rescue ReactOnRailsPro::Error; raise block is redundant in license_validator.rb:59.

7. Missing TypeScript Type Safety

licenseValidator.ts:150 uses (license as any).license_type - consider adding proper interface fields.

8. CI Documentation Accuracy

CI_SETUP.md line 349 suggests validation returns false, but it actually raises exceptions on invalid licenses.

📊 Test Coverage Analysis

Ruby Tests: ~95% coverage

Valid licenses, expired licenses, missing exp field, invalid signatures, missing licenses, config file loading, caching, error messages

TypeScript Tests: ~95% coverage

All major scenarios covered with proper mocking of process.exit()

🔒 Security Assessment

Overall: GOOD

The implementation follows JWT security best practices:

  • ✅ Algorithm explicitly specified (prevents algorithm substitution)
  • ✅ RSA-256 signatures (strong asymmetric crypto)
  • ✅ Proper expiration handling
  • ✅ No secrets in code (only public key)
  • ✅ Offline validation (privacy-friendly)
  • ✅ Clear error messages without leaking sensitive data

📝 Final Verdict

This is a well-engineered solution with solid security foundations and comprehensive documentation.

Main blocking issues:

  1. Node error handling inconsistency (development mode unreachable)
  2. Missing trailing newlines (CLAUDE.md compliance)
  3. Placeholder keys need replacement
  4. RuboCop must be run

Once these are addressed, this PR will be in excellent shape for merge.

Estimated Review Score: 8.5/10

Great work! 🎉

Copy link
Contributor

@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: 6

♻️ Duplicate comments (2)
react_on_rails_pro/LICENSE_SETUP.md (1)

121-123: Correct the Ruby console example to the actual API

Use the proper module and method name.

-> ReactOnRails::Utils.react_on_rails_pro_licence_valid?
+> ReactOnRailsPro::Utils.licence_valid?
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)

137-141: Config-file stubbing won’t apply to new Pathname instances

The code builds a fresh Pathname with Rails.root.join; stub on a specific instance may not intercept. Stub any Pathname or the exact File.read path.

-        allow(config_path).to receive(:exist?).and_return(true)
-        allow(File).to receive(:read).with(config_path).and_return(valid_token)
+        allow_any_instance_of(Pathname).to receive(:exist?).and_return(true)
+        allow(File).to receive(:read)
+          .with(Rails.root.join("config", "react_on_rails_pro_license.key"))
+          .and_return(valid_token)
🧹 Nitpick comments (13)
react_on_rails_pro/CI_SETUP.md (2)

361-362: Avoid printing full JWT payload in CI logs

Decoding and printing can expose PII (email). Log minimal, masked fields instead or run locally.

Example:

bundle exec rails runner "require 'jwt'; d=JWT.decode(ENV['REACT_ON_RAILS_PRO_LICENSE'], nil, false).first; puts({sub: (d['sub']&.gsub(/.(?=.{3})/,'*')), exp: d['exp'], license_type: d['license_type']}.to_json)"

469-469: Wrap support email to satisfy markdownlint (MD034) and clarity

Use a mailto link.

-- **Email Support**: [email protected]
+- **Email Support**: [[email protected]](mailto:[email protected])
react_on_rails_pro/LICENSE_SETUP.md (1)

214-214: Format support email as a link

Improves clarity and satisfies markdownlint (MD034).

-3. **Email**: [email protected]
+3. **Email**: [[email protected]](mailto:[email protected])
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (3)

33-37: Unreachable branch due to raise in load_license_string

load_and_decode_license raises on missing license; this early return won’t be hit.

-          # If no license found, load_license_string already handled the error
-          return false unless license
+          # load_and_decode_license raises on missing license via handle_invalid_license

95-97: Trim ENV-sourced license to avoid decode failures from whitespace

CI secrets often include trailing newlines/spaces; strip before use.

-        license = ENV["REACT_ON_RAILS_PRO_LICENSE"]
+        license = ENV["REACT_ON_RAILS_PRO_LICENSE"]&.strip
         return license if license.present?

80-91: Optional: Validate issuer/audience to harden JWT verification

If licenses include iss/aud, enable checks to prevent token confusion.

Example (adjust claims/values to your issuance):

JWT.decode(
  license_string,
  public_key,
  true,
  algorithm: "RS256",
  verify_expiration: false,
  verify_iss: true, iss: "shakacode.com",
  # verify_aud: true, aud: "react-on-rails-pro"
).first

Based on learnings

react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (3)

7-9: Remove redundant mocking of licensePublicKey

Using both jest.mock and jest.doMock for the same module is unnecessary and can confuse module state.

-jest.mock('../src/shared/licensePublicKey', () => ({
-  PUBLIC_KEY: ''
-}));
...
-    jest.doMock('../src/shared/licensePublicKey', () => ({
-      PUBLIC_KEY: testPublicKey
-    }));
+    jest.doMock('../src/shared/licensePublicKey', () => ({
+      PUBLIC_KEY: testPublicKey
+    }));

Also applies to: 37-39


16-20: Make process.exit assertions deterministic by setting NODE_ENV

Explicitly set production in tests that expect exit; restore afterward.

   beforeEach(() => {
+    process.env.NODE_ENV = 'production';
     // Clear the module cache to get a fresh instance
     jest.resetModules();
@@
   afterEach(() => {
     delete process.env.REACT_ON_RAILS_PRO_LICENSE;
+    delete process.env.NODE_ENV;
     jest.restoreAllMocks();
   });

Also applies to: 52-55


197-217: Optional: Reduce RSA key generation overhead

Generating keys per test is slow. Generate once in beforeAll and reuse unless a test needs a different key.

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (4)

103-106: Add issuer and audience validation to strengthen JWT verification.

According to JWT best practices and the learnings from the jsonwebtoken library documentation, you should validate issuer and audience claims when applicable to prevent token misuse.

Based on learnings from jsonwebtoken documentation.

       const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
-        algorithms: ['RS256']
+        algorithms: ['RS256'],
+        issuer: 'shakacode.com',  // Adjust to actual issuer value
+        clockTolerance: 60  // Allow 60 seconds clock skew
       }) as LicenseData;

If audience is included in your license tokens, add it as well. This prevents tokens issued for other purposes from being accepted.


124-124: Consider making the license file path configurable.

Using process.cwd() assumes the current working directory is the project root, which may not hold true in all deployment scenarios (e.g., when the application is started from a different directory or in containerized environments).

Consider one of these approaches:

  1. Accept an optional base path parameter (via constructor or environment variable)
  2. Search up the directory tree for a package.json to find the project root
  3. Document the path resolution behavior clearly

Example for environment variable approach:

-      const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
+      const basePath = process.env.REACT_ON_RAILS_PRO_BASE_PATH || process.cwd();
+      const configPath = path.join(basePath, 'config', 'react_on_rails_pro_license.key');

83-94: Consider extracting the ShakaCode URL to a constant.

The URL https://shakacode.com/react-on-rails-pro is repeated in multiple error messages throughout the file (lines 65, 73, 87, 90, 135). Extracting it to a constant improves maintainability.

+const SHAKACODE_LICENSE_URL = 'https://shakacode.com/react-on-rails-pro';
+
 class LicenseValidator {
   // ... existing code ...
   
         this.validationError = 'License is missing required expiration field. ' +
                                'Your license may be from an older version. ' +
-                               'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';
+                               `Get a FREE evaluation license at ${SHAKACODE_LICENSE_URL}`;

Apply similar changes to other error messages at lines 73, 87, 90, and 135.


6-11: Consider defining specific optional fields for better type safety.

The [key: string]: any index signature makes the interface very permissive. If there are known optional fields (like license_type used at line 150), consider defining them explicitly.

 interface LicenseData {
   sub?: string;
   iat?: number;
   exp: number; // Required: expiration timestamp
+  license_type?: string;
+  iss?: string;  // issuer
+  aud?: string;  // audience
   [key: string]: any;
 }

This provides better autocomplete and type checking while maintaining flexibility for unknown fields.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7fc4f0 and e2af939.

📒 Files selected for processing (6)
  • react_on_rails_pro/CI_SETUP.md (1 hunks)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/CI_SETUP.md
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
  • react_on_rails_pro/LICENSE_SETUP.md
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
🧠 Learnings (2)
📚 Learning: 2025-02-13T14:29:49.267Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: spec/react_on_rails/utils_spec.rb:218-218
Timestamp: 2025-02-13T14:29:49.267Z
Learning: In RSpec tests, prefer using local variables over constants within test blocks to avoid constant redefinition warnings and maintain better test isolation.

Applied to files:

  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
🧬 Code graph analysis (3)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (6-14)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (6)
  • public_key (109-111)
  • reset! (15-19)
  • valid? (7-128)
  • valid? (9-13)
  • license_data (21-23)
  • validation_error (25-27)
react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
  • licenseValidator (157-157)
🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/CI_SETUP.md

214-214: Bare URL used

(MD034, no-bare-urls)

react_on_rails_pro/LICENSE_SETUP.md

214-214: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: build
  • GitHub Check: claude-review
  • GitHub Check: markdown-link-check
  • GitHub Check: build-and-test
🔇 Additional comments (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)

1-4: LGTM: Clean imports with appropriate dependencies.

The imports are well-organized and include all necessary dependencies for JWT validation and file system operations.

Comment on lines 349 to 350
bundle exec rails runner "puts ReactOnRails::Utils.react_on_rails_pro_licence_valid? ? '✅ License valid' : '❌ License invalid'"
```
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Ruby API reference in verification snippet

Use the actual helper API and namespace.

-bundle exec rails runner "puts ReactOnRails::Utils.react_on_rails_pro_licence_valid? ? '✅ License valid' : '❌ License invalid'"
+bundle exec rails runner "puts ReactOnRailsPro::Utils.licence_valid? ? '✅ License valid' : '❌ License invalid'"
📝 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
bundle exec rails runner "puts ReactOnRails::Utils.react_on_rails_pro_licence_valid? ? '✅ License valid' : '❌ License invalid'"
```
bundle exec rails runner "puts ReactOnRailsPro::Utils.licence_valid? ? '✅ License valid' : '❌ License invalid'"
🤖 Prompt for AI Agents
In react_on_rails_pro/CI_SETUP.md around lines 349-350, the verification snippet
is using a non-existent Utils API and British spelling; replace the call with
the actual helper namespace and method (ReactOnRails::Pro.license_valid?) and
update the printed message accordingly so the snippet invokes the real Ruby API
for verifying the Pro license.

Comment on lines 37 to 42
public getLicenseData(): LicenseData | undefined {
if (!this.licenseData) {
this.loadAndDecodeLicense();
}
return this.licenseData;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Unhandled errors can crash the application.

getLicenseData() calls loadAndDecodeLicense() which can throw errors (line 111), but no error handling is present. If called before isValid(), this will result in an unhandled exception.

Wrap the call in a try-catch block:

 public getLicenseData(): LicenseData | undefined {
-  if (!this.licenseData) {
-    this.loadAndDecodeLicense();
+  if (!this.licenseData) {
+    try {
+      this.loadAndDecodeLicense();
+    } catch (error) {
+      // Return undefined on error; validation state tracked separately
+      return undefined;
+    }
   }
   return this.licenseData;
 }
📝 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
public getLicenseData(): LicenseData | undefined {
if (!this.licenseData) {
this.loadAndDecodeLicense();
}
return this.licenseData;
}
public getLicenseData(): LicenseData | undefined {
if (!this.licenseData) {
try {
this.loadAndDecodeLicense();
} catch (error) {
// Return undefined on error; validation state tracked separately
return undefined;
}
}
return this.licenseData;
}
🤖 Prompt for AI Agents
In react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
around lines 37 to 42, getLicenseData() currently calls loadAndDecodeLicense()
which can throw (see line 111); wrap that call in a try-catch so any thrown
errors are caught and do not bubble up and crash the app—on catch, log the error
(or use the existing logger) and return undefined so callers safely receive no
license data instead of an unhandled exception.

Comment on lines 70 to 77
// Check expiry
if (Date.now() / 1000 > license.exp) {
this.validationError = 'License has expired. ' +
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
'or upgrade to a paid license for production use.';
this.handleInvalidLicense(this.validationError);
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major: Expiry check lacks clock tolerance for clock skew.

The manual expiry comparison doesn't account for clock differences between systems, which can cause false rejections. The jsonwebtoken library supports a clockTolerance option for this purpose.

Move the expiry check into jwt.verify() options instead of manual comparison:

-      // Check expiry
-      if (Date.now() / 1000 > license.exp) {
-        this.validationError = 'License has expired. ' +
-                               'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
-                               'or upgrade to a paid license for production use.';
-        this.handleInvalidLicense(this.validationError);
-        return false;
-      }

And update jwt.verify() at lines 104-106:

       const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
-        algorithms: ['RS256']
+        algorithms: ['RS256'],
+        clockTolerance: 60 // Allow 60 seconds clock skew
       }) as LicenseData;

The library will throw a TokenExpiredError with a clear message when the token is expired (accounting for clock skew).

📝 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
// Check expiry
if (Date.now() / 1000 > license.exp) {
this.validationError = 'License has expired. ' +
'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
'or upgrade to a paid license for production use.';
this.handleInvalidLicense(this.validationError);
return false;
}
// --- Remove the manual expiry check around line 70 ---
- // Check expiry
- if (Date.now() / 1000 > license.exp) {
- this.validationError = 'License has expired. ' +
- 'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
- 'or upgrade to a paid license for production use.';
- this.handleInvalidLicense(this.validationError);
- return false;
- }
// --- Update the jwt.verify call around lines 104–106 ---
const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
algorithms: ['RS256'],
clockTolerance: 60 // Allow 60 seconds clock skew
}) as LicenseData;
🤖 Prompt for AI Agents
In react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
around lines 70 to 77 and the jwt.verify call at lines ~104-106, the manual
expiry comparison using Date.now() should be removed and replaced by passing a
clockTolerance option to jwt.verify so token expiry is validated by the
jsonwebtoken library (which will throw TokenExpiredError when appropriate,
accounting for clock skew); update the jwt.verify() invocation to include {
clockTolerance: <seconds> } (choose a sensible tolerance like 30 or 60), remove
the manual expiry branch, and ensure TokenExpiredError is handled in the
existing catch/invalid-license flow to produce the same validationError path.

Comment on lines 115 to 142
private loadLicenseString(): string | undefined {
// First try environment variable
const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE;
if (envLicense) {
return envLicense;
}

// Then try config file (relative to project root)
try {
const configPath = path.join(process.cwd(), 'config', 'react_on_rails_pro_license.key');
if (fs.existsSync(configPath)) {
return fs.readFileSync(configPath, 'utf8').trim();
}
} catch (error) {
// File doesn't exist or can't be read
}

this.validationError =
'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
'or create config/react_on_rails_pro_license.key file. ' +
'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro';

this.handleInvalidLicense(this.validationError);

return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unreachable return statement after process.exit() call.

Similar to validateLicense(), this method returns undefined after calling handleInvalidLicense() (line 137), but the return is never reached due to process.exit() on line 146.

Once you refactor handleInvalidLicense() to not exit the process (as suggested earlier), this return statement will become meaningful.

🤖 Prompt for AI Agents
In react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
around lines 115 to 140, the method currently calls handleInvalidLicense() which
exits the process, making the subsequent "return undefined" unreachable;
refactor handleInvalidLicense to not call process.exit() (instead return a value
or throw a controlled error) and update its callers to handle that
return/exception; then leave the "return undefined" in loadLicenseString as the
normal non-exceptional fallback so the function can return cleanly when no
license is found.

Copy link
Contributor

@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

🧹 Nitpick comments (1)
react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1)

22-25: Redundant logging on successful validation.

Line 22 logs "Validating license..." and line 25 logs "License validation successful." If validation is fast, the first message may not provide value and could clutter logs.

Consider removing the initial log message to reduce noise:

-        Rails.logger.info "[React on Rails Pro] Validating license..."
-
         if ReactOnRailsPro::LicenseValidator.valid?
           Rails.logger.info "[React on Rails Pro] License validation successful"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2af939 and aa029fa.

📒 Files selected for processing (2)
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/master.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails_pro/packages/node-renderer/src/master.ts
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb
🧠 Learnings (1)
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: claude-review
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: build
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: markdown-link-check
  • GitHub Check: build-and-test
🔇 Additional comments (1)
react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1)

27-29: LicenseValidator.valid? raises on failures, so the else branch is unreachable and the comment is accurate. No changes needed.

Likely an incorrect or invalid review comment.

initializer "react_on_rails_pro.validate_license", before: :load_config_initializers do
config.after_initialize do
# Skip license validation during asset precompilation
next if defined?(Rake) && Rake::Task.task_defined?("assets:precompile")
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Rake task check may not detect active precompilation.

Rake::Task.task_defined?("assets:precompile") checks whether the task is defined, not whether it's currently running. If the task is defined but not executing (e.g., in a Rails console), validation will still be skipped incorrectly.

Consider checking the current Rake task name instead:

-        next if defined?(Rake) && Rake::Task.task_defined?("assets:precompile")
+        next if defined?(Rake) && ARGV.any? { |arg| arg.start_with?("assets:") }

Alternatively, verify the intended behavior—if skipping whenever the task is defined (regardless of execution) is acceptable, document this explicitly in the comment.


🏁 Script executed:

#!/bin/bash
rg -n "'assets:precompile'" -C5 --glob 'react_on_rails_pro/lib/react_on_rails_pro/engine.rb'

Length of output: 0


🏁 Script executed:

#!/bin/bash
rg -n 'Rake::Task.task_defined' -C5 --glob 'react_on_rails_pro/lib/react_on_rails_pro/engine.rb'

Length of output: 1146


Skip license validation only during asset precompilation
Replace the definition check with a runtime task check:

- next if defined?(Rake) && Rake::Task.task_defined?("assets:precompile")
+ next if defined?(Rake) && Rake.application.top_level_tasks.include?("assets:precompile")
🤖 Prompt for AI Agents
In react_on_rails_pro/lib/react_on_rails_pro/engine.rb around line 16, replace
the current definition check with a runtime task check: change the line to call
Rake::Task.task_defined?("assets:precompile") and guard against Rake not being
loaded by rescuing NameError (e.g. use `next if
Rake::Task.task_defined?("assets:precompile") rescue false`) so we skip license
validation only when the assets:precompile task actually exists at runtime.

Copy link
Contributor

@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 (7)
react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1)

13-25: Timing configuration inconsistency remains unresolved.

The initializer is still declared with before: :load_config_initializers but wraps the validation logic inside config.after_initialize. This creates a timing contradiction—validation will run after all initializers complete, not before load_config_initializers as the option suggests.

Additionally, the else branch (lines 19-22) is unreachable because ReactOnRailsPro::LicenseValidator.valid? raises a ReactOnRailsPro::Error when validation fails (see license_validator.rb lines 116).

Apply this diff to fix the timing and remove unreachable code:

-    initializer "react_on_rails_pro.validate_license", before: :load_config_initializers do
-      config.after_initialize do
-        Rails.logger.info "[React on Rails Pro] Validating license..."
+    initializer "react_on_rails_pro.validate_license", before: :load_config_initializers do
+      Rails.logger.info "[React on Rails Pro] Validating license..."

-        if ReactOnRailsPro::LicenseValidator.valid?
-          Rails.logger.info "[React on Rails Pro] License validation successful"
-        else
-          # License validation will raise an error, so this line won't be reached
-          # But we include it for clarity
-          Rails.logger.error "[React on Rails Pro] License validation failed"
-        end
-      end
+      if ReactOnRailsPro::LicenseValidator.valid?
+        Rails.logger.info "[React on Rails Pro] License validation successful"
+      end
     end
react_on_rails_pro/LICENSE_SETUP.md (1)

119-123: Incorrect method reference in documentation.

The method name is incorrect. Based on the implementation, it should be ReactOnRailsPro::Utils.licence_valid? (not ReactOnRails::Utils.react_on_rails_pro_licence_valid?).

Apply this diff:

-> ReactOnRails::Utils.react_on_rails_pro_licence_valid?
+> ReactOnRailsPro::Utils.licence_valid?
 # Should return: true
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (5)

37-42: Unhandled errors can crash the application.

loadAndDecodeLicense() can throw errors (line 111), but no error handling is present in getLicenseData(). If called before isValid(), this will result in an unhandled exception.

Wrap the call in a try-catch block:

 public getLicenseData(): LicenseData | undefined {
   if (!this.licenseData) {
-    this.loadAndDecodeLicense();
+    try {
+      this.loadAndDecodeLicense();
+    } catch (error) {
+      // Return undefined on error; validation state tracked separately
+      return undefined;
+    }
   }
   return this.licenseData;
 }

54-95: Unreachable return statements after process.exit() calls.

The method returns false after calling handleInvalidLicense(), but since handleInvalidLicense() exits the process (line 146), these return statements are never reached.

Once you refactor handleInvalidLicense() to not exit the process (as suggested in the comment for lines 142-147), these return statements will become meaningful. The preferred solution is to remove the process.exit() from handleInvalidLicense() and let the caller (e.g., master.ts) decide whether to exit based on environment.


70-77: Expiry check lacks clock tolerance for clock skew.

The manual expiry comparison doesn't account for clock differences between systems, which can cause false rejections. The jsonwebtoken library supports a clockTolerance option for this purpose.

Move the expiry check into jwt.verify() options:

-      // Check expiry
-      if (Date.now() / 1000 > license.exp) {
-        this.validationError = 'License has expired. ' +
-                               'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
-                               'or upgrade to a paid license for production use.';
-        this.handleInvalidLicense(this.validationError);
-        return false;
-      }

And update jwt.verify() at lines 104-106:

       const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
-        algorithms: ['RS256']
+        algorithms: ['RS256'],
+        clockTolerance: 60 // Allow 60 seconds clock skew
       }) as LicenseData;

The library will throw a TokenExpiredError with a clear message when the token is expired (accounting for clock skew).

Based on learnings.


142-147: Unconditional process exit breaks testing and development workflows.

The handleInvalidLicense() method always exits the process, which:

  • Makes the validator untestable (tests cannot verify error handling without mocking process.exit)
  • Prevents development workflows where invalid licenses should warn but not crash
  • Violates separation of concerns—libraries should not control process lifecycle

Remove the process.exit() and let the caller decide:

 private handleInvalidLicense(message: string): void {
   const fullMessage = `[React on Rails Pro] ${message}`;
   console.error(fullMessage);
-  // Validation errors should prevent the application from starting
-  process.exit(1);
+  // Let the caller decide whether to exit (e.g., master.ts checks environment)
 }

Then in master.ts, handle the exit decision based on environment:

if (!isLicenseValid()) {
  const error = getLicenseValidationError();
  console.error(error);
  if (process.env.NODE_ENV === 'production') {
    process.exit(1);
  }
}

115-140: Unreachable return statement after process exit.

The return undefined on line 139 is unreachable because handleInvalidLicense() (line 137) exits the process (line 146).

Once you refactor handleInvalidLicense() to not exit the process, this return statement will become meaningful.

🧹 Nitpick comments (2)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1)

46-53: Recommend adding clock tolerance for expiration check.

The manual expiry comparison doesn't account for clock skew between systems, which can cause false rejections. While the jwt gem's verify_expiration: false disables automatic checking (line 89), consider adding a tolerance window to the manual check.

Apply this diff to add clock tolerance:

       # Check expiry
-      if Time.now.to_i > license["exp"]
+      # Allow 60 seconds clock tolerance to handle clock skew between systems
+      if Time.now.to_i > (license["exp"] + 60)
         @validation_error = "License has expired. " \
                            "Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro " \
                            "or upgrade to a paid license for production use."

Alternatively, enable automatic verification with tolerance in JWT.decode (line 80-90) if you prefer to let the library handle it.

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)

103-112: LGTM! Good JWT security practices.

The implementation correctly:

  • Hardcodes the algorithms array to prevent algorithm confusion attacks
  • Uses RS256 with an asymmetric public key
  • Follows jsonwebtoken v9.0.2 security defaults (enforces RSA >= 2048 bits, rejects unsigned tokens)

Optional: Consider validating standard claims if your license generation includes them:

const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
  algorithms: ['RS256'],
  clockTolerance: 60,
  issuer: 'shakacode.com', // If licenses include 'iss' claim
  // audience: 'react-on-rails-pro' // If licenses include 'aud' claim
}) as LicenseData;

Based on learnings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa029fa and 7b21ef2.

📒 Files selected for processing (4)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/LICENSE_SETUP.md
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
🧠 Learnings (1)
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
🧬 Code graph analysis (2)
react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)
  • valid? (7-129)
  • valid? (9-13)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (6-14)
🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/LICENSE_SETUP.md

215-215: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (9)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: build
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
🔇 Additional comments (5)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (5)

9-13: LGTM! Proper memoization pattern.

The caching strategy is appropriate for license validation, avoiding repeated JWT decoding.


15-19: LGTM! Clean reset implementation.

Properly clears all cached state, which is essential for testing scenarios.


76-91: LGTM! Excellent security practices.

The implementation correctly:

  • Hardcodes the algorithm to prevent algorithm confusion attacks
  • Includes a security comment with reference to Auth0's vulnerability disclosure
  • Disables automatic expiration to enable custom error messaging

This follows JWT security best practices.


113-117: LGTM! Clear error handling.

Proper logging and exception raising with descriptive messages.


119-127: LGTM! Safe optional field logging.

Proper nil checks and conditional logging for analytics data.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore (1)

48-49: Anchor license ignore path and secure packaging

  • In react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore, change
    config/react_on_rails_pro_license.key/config/react_on_rails_pro_license.key
  • Mirror the same update in react_on_rails_pro/spec/dummy/.gitignore
  • Add to root .gitignore:
    **/config/react_on_rails_pro_license.key
  • In both gemspecs, exclude config/react_on_rails_pro_license.key from packaged files
  • In .npmignore (root & react_on_rails_pro/.npmignore), add:
    /config/react_on_rails_pro_license.key
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b21ef2 and 7483624.

⛔ Files ignored due to path filters (3)
  • react_on_rails_pro/spec/dummy/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • react_on_rails_pro/spec/execjs-compatible-dummy/Gemfile.lock is excluded by !**/*.lock
  • react_on_rails_pro/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • react_on_rails_pro/package.json (2 hunks)
  • react_on_rails_pro/spec/dummy/.gitignore (1 hunks)
  • react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore (1 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • react_on_rails_pro/spec/dummy/.gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails_pro/package.json
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.
📚 Learning: 2025-02-12T16:38:06.537Z
Learnt from: Romex91
PR: shakacode/react_on_rails#1697
File: package-scripts.yml:28-28
Timestamp: 2025-02-12T16:38:06.537Z
Learning: The file `node_package/lib/ReactOnRails.full.js` is autogenerated during the build process and should not be present in the repository.

Applied to files:

  • react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore
⏰ 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). (9)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: build
  • GitHub Check: build-dummy-app-webpack-test-bundles (3.4, 22)
  • GitHub Check: claude-review
  • GitHub Check: markdown-link-check
  • GitHub Check: build-and-test

Copy link

claude bot commented Oct 12, 2025

Pull Request Review - Offline JWT License Validation

Comprehensive review of PR 1857 completed. Overall this is a well-architected and secure implementation with excellent test coverage.

STRENGTHS:

  • Excellent JWT implementation with RS256 algorithm hardcoded to prevent algorithm confusion attacks
  • Secure key management with private key never committed
  • Offline validation with no external runtime dependencies
  • Consistent Ruby and TypeScript implementations
  • Comprehensive test coverage with 205 lines Ruby specs and 262 lines TypeScript tests
  • All critical scenarios covered in tests
  • Good documentation with detailed setup guides

CRITICAL ISSUES:

  1. Development vs Production Behavior Mismatch

    • PR description states warnings in development errors in production
    • Actual implementation raises errors in ALL environments
    • licenseValidator.ts:146 always calls process.exit(1) regardless of NODE_ENV
    • license_validator.rb:116 always raises error regardless of Rails.env
    • Recommendation: Align implementation with stated behavior
  2. Cached Validation Never Expires

    • Both implementations cache validation results indefinitely
    • If license validated at 11:59 PM and expires at midnight cached result stays valid
    • Recommendation: Add TTL-based cache expiration

MEDIUM PRIORITY:

  1. File I/O on Every Failed Validation

    • When ENV var not set code reads from disk on every validation attempt
    • Recommendation: Cache the loaded license string
  2. Rails Engine Initializer Timing

    • engine.rb:13 uses confusing before parameter with after_initialize
    • Recommendation: Simplify initializer code
  3. Inconsistent Test Assertions

    • TypeScript test expects different error message than what validator sets
    • Recommendation: Fix test expectations

MINOR ISSUES:

  1. Pre-commit Requirements from CLAUDE.md

    • Run bundle exec rubocop and fix all violations
    • Verify all files end with trailing newlines
    • Run rake lint
  2. Missing Test Coverage

    • Clock skew scenarios
    • Malformed JWT strings
    • Full integration tests

SECURITY ASSESSMENT:
Strong implementation with algorithm hardcoded to RS256 public key cryptography no secrets in code proper error handling. Recommendations include documenting key rotation procedure and considering license revocation capability.

PERFORMANCE:
Positive singleton pattern JWT verification is fast no network calls. Could improve cache expiration and file I/O optimization.

RECOMMENDATION: Approve with minor changes primarily addressing dev vs prod behavior discrepancy and cache expiration issue.

Generated with Claude Code

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)

160-204: Consider optional test enhancements.

The tests for license_data, validation_error, and reset! are functional. A few optional improvements:

  1. Test license_data caching: Verify memoization by checking that load_and_decode_license is called once across multiple license_data calls.
  2. Test optional license fields: Add tests with plan and issued_by fields to verify the log_license_info behavior.
  3. Refactor repeated assertions: Lines 81-83, 105-107, 121-123, 136-139 all verify FREE license messaging. Consider extracting to a shared example.
  4. Style note (lines 182-186): The begin/rescue pattern works but feels awkward. An alternative could be a separate test or using aggregate_failures.

Example shared example for FREE license messaging:

shared_examples "includes FREE license guidance" do
  it "includes FREE license information in error message" do
    expect { described_class.valid? }.to raise_error(ReactOnRailsPro::Error, /FREE evaluation license/)
  end
end

Then in each error context:

context "with expired license" do
  # ... setup ...
  
  it "raises error" do
    expect { described_class.valid? }.to raise_error(ReactOnRailsPro::Error, /License has expired/)
  end
  
  include_examples "includes FREE license guidance"
end
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7483624 and 7125571.

📒 Files selected for processing (1)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
🧠 Learnings (1)
📚 Learning: 2025-02-13T14:29:49.267Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: spec/react_on_rails/utils_spec.rb:218-218
Timestamp: 2025-02-13T14:29:49.267Z
Learning: In RSpec tests, prefer using local variables over constants within test blocks to avoid constant redefinition warnings and maintain better test isolation.

Applied to files:

  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
🧬 Code graph analysis (1)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (6)
  • public_key (109-111)
  • reset! (15-19)
  • valid? (7-129)
  • valid? (9-13)
  • license_data (21-23)
  • validation_error (25-27)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: claude-review
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: build
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: build-and-test
🔇 Additional comments (3)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (3)

35-46: Previous issues successfully addressed.

The use of stub_const for the public key constant (line 38) and the Rails mock setup correctly resolve the issues from the previous review.


53-140: LGTM: Comprehensive validation test coverage.

The test contexts thoroughly cover all validation scenarios—valid licenses, expiration, missing fields, invalid signatures, and missing licenses. Error message assertions correctly verify both the specific failure reason and the FREE license guidance.


142-157: Config file path stubbing now correct.

The test properly creates a separate file_config_path double with exist?: true and overrides the mock_root.join behavior specifically for this test. The File.read stub is correctly scoped to the new path. This resolves the previous review concern.

Copy link
Contributor

@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

🧹 Nitpick comments (1)
react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1)

190-210: Consider clarifying the caching test's intent.

The test correctly validates that cached results persist after environment changes. However, the test could be more explicit about verifying the cache mechanism itself.

Consider adding a comment or spy to document that the test validates "cached result persists even when ENV is deleted":

     // First call
     expect(module.isLicenseValid()).toBe(true);
 
-    // Change ENV (shouldn't affect cached result)
+    // Delete ENV - cached result should persist
     delete process.env.REACT_ON_RAILS_PRO_LICENSE;
 
-    // Second call should use cache
+    // Second call should return cached true (not re-validate)
     expect(module.isLicenseValid()).toBe(true);

Alternatively, spy on the internal validation logic to verify it's not called on the second invocation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7125571 and bd83e3c.

📒 Files selected for processing (3)
  • react_on_rails_pro/.gitignore (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
🧬 Code graph analysis (1)
react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
  • licenseValidator (164-164)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
  • GitHub Check: build
  • GitHub Check: build-and-test
🔇 Additional comments (5)
react_on_rails_pro/.gitignore (1)

76-77: Good call on ignoring the license key.

Helps keep the signed license artifact out of source control; change looks solid.

react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (4)

16-64: LGTM: Comprehensive test setup with proper isolation.

The test setup correctly:

  • Generates RSA key pairs with 2048-bit modulus (aligns with jsonwebtoken v9.x minimum requirement, as per learnings)
  • Uses jest.resetModules() + dynamic require() for per-test module isolation
  • Mocks process.exit, console.error, and console.log to prevent test interruption
  • Dynamically mocks the public key module with test keys

Note: Mocking console.error/log (lines 27-28) keeps test output clean but may make debugging test failures harder. This is a reasonable trade-off for expected error scenarios.

Based on learnings


72-84: LGTM: Valid license scenario is properly tested.


86-167: LGTM: Error scenarios are thoroughly tested.

All failure modes correctly verify:

  • process.exit(1) is called
  • Error messages contain expected content
  • Free evaluation license notice is displayed

The invalid signature test (lines 126-152) properly generates a different key pair to ensure signature verification fails.


213-252: LGTM: getLicenseData and getLicenseValidationError tests are clear and correct.

Both tests properly verify:

  • getLicenseData() returns decoded JWT payload with standard and custom fields
  • getLicenseValidationError() returns the error message for failed validation

AbanoubGhadban and others added 14 commits October 13, 2025 13:17
Implements a pure offline license validation system using JWT tokens signed
with RSA-256. No internet connectivity required for validation.

Key features:
- JWT-based licenses verified with embedded public key (RSA-256)
- Offline validation in Ruby gem and Node renderer
- Environment variable or config file support
- Development-friendly (warnings) vs production (errors)
- Zero impact on browser bundle size
- Comprehensive test coverage

Changes:
- Add JWT dependencies (Ruby jwt gem, Node jsonwebtoken)
- Create license validation modules for Ruby and Node
- Integrate validation into Rails context (rorPro field)
- Add license check on Node renderer startup
- Update .gitignore for license file
- Add comprehensive tests for both Ruby and Node
- Create LICENSE_SETUP.md documentation

The system validates licenses at:
1. Ruby gem initialization (Rails startup)
2. Node renderer startup
3. Browser relies on server validation (railsContext.rorPro)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Breaking Change: All environments now require valid licenses
- Add ignoreExpiration: true to jwt.verify() to match Ruby behavior
- Mock process.exit globally in tests to prevent actual exit
- Mock console.error and console.log to suppress test output
- Update all invalid license tests to check process.exit was called
- Simplify file-based license test to use ENV variable
- All 9 Node.js tests now passing

Changes align Node.js validator with Ruby validator:
- Both manually check expiration after disabling auto-check
- Both call exit/raise on invalid licenses
- Both provide consistent error messages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@AbanoubGhadban AbanoubGhadban force-pushed the feature/pro-license-validation branch from bd83e3c to 9650099 Compare October 13, 2025 10:34
Copy link
Contributor

@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 (7)
react_on_rails_pro/LICENSE_SETUP.md (1)

119-123: Correct the Ruby API reference in the verification example.

The method name and namespace are incorrect. Use ReactOnRailsPro::Utils.licence_valid? instead.

Apply this diff:

-> ReactOnRails::Utils.react_on_rails_pro_licence_valid?
+> ReactOnRailsPro::Utils.licence_valid?
 # Should return: true
react_on_rails_pro/CI_SETUP.md (1)

348-350: Correct the Ruby API reference in the verification snippet.

The method name and namespace are incorrect. Use ReactOnRailsPro::Utils.licence_valid? instead.

Apply this diff:

-bundle exec rails runner "puts ReactOnRails::Utils.react_on_rails_pro_licence_valid? ? '✅ License valid' : '❌ License invalid'"
+bundle exec rails runner "puts ReactOnRailsPro::Utils.licence_valid? ? '✅ License valid' : '❌ License invalid'"
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1)

102-106: Remove unreachable return statement.

The nil return on line 106 is unreachable because handle_invalid_license (line 105) raises a ReactOnRailsPro::Error (line 116).

Apply this diff:

       @validation_error = "No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable " \
                          "or create config/react_on_rails_pro_license.key file. " \
                          "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"
       handle_invalid_license(@validation_error)
-      nil
+      # handle_invalid_license raises an error; this line is unreachable
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (4)

54-95: Unreachable return statements after error handling.

The validateLicense() method returns false after calling handleInvalidLicense(), but since handleInvalidLicense() exits the process (line 148), these return statements are never reached. Once you refactor handleInvalidLicense() to not exit the process (as suggested above), these return statements will become meaningful.

No code change needed now - this will be resolved once the handleInvalidLicense() refactor is complete.


37-42: Add error handling to prevent unhandled exceptions.

getLicenseData() calls loadAndDecodeLicense() which can throw errors (line 113), but no error handling is present. If called before isValid(), this will result in an unhandled exception.

Wrap the call in a try-catch block:

 public getLicenseData(): LicenseData | undefined {
   if (!this.licenseData) {
-    this.loadAndDecodeLicense();
+    try {
+      this.loadAndDecodeLicense();
+    } catch (error) {
+      // Return undefined on error; validation state tracked separately
+      return undefined;
+    }
   }
   return this.licenseData;
 }

70-77: Add clock tolerance to expiry validation.

The manual expiry check doesn't account for clock skew between systems. Based on learnings, the jsonwebtoken library (v9.x) supports a clockTolerance option.

Remove the manual check and use the library's built-in validation with clock tolerance:

Remove lines 70-77 (manual expiry check), and update jwt.verify() at lines 104-108:

       const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
         algorithms: ['RS256'],
-        // Disable automatic expiration verification so we can handle it manually with custom logic
-        ignoreExpiration: true
+        clockTolerance: 60 // Allow 60 seconds clock skew
       }) as LicenseData;

Then handle TokenExpiredError in the catch block at line 83:

     } catch (error: any) {
-      if (error.name === 'JsonWebTokenError') {
+      if (error.name === 'TokenExpiredError') {
+        this.validationError = 'License has expired. ' +
+                               'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
+                               'or upgrade to a paid license for production use.';
+      } else if (error.name === 'JsonWebTokenError') {
         this.validationError = `Invalid license signature: ${error.message}. ` +

144-149: Remove unconditional process.exit() to enable testing and flexible error handling.

The handleInvalidLicense() method always exits the process, which makes the validator untestable and prevents development workflows where invalid licenses should warn but not crash. According to the PR context, master.ts should handle environment-specific behavior.

Remove the process.exit() call and let the caller decide:

 private handleInvalidLicense(message: string): void {
   const fullMessage = `[React on Rails Pro] ${message}`;
   console.error(fullMessage);
-  // Validation errors should prevent the application from starting
-  process.exit(1);
+  // Let the caller decide whether to exit (e.g., master.ts checks environment)
 }

Then update master.ts to handle the exit decision based on environment (this logic already exists at lines 18-24 in master.ts, so no change needed there once this is fixed).

🧹 Nitpick comments (1)
react_on_rails_pro/packages/node-renderer/src/master.ts (1)

18-24: Remove redundant process.exit() call.

The comment states that handleInvalidLicense already calls process.exit(1) (which it does at line 148 in licenseValidator.ts), making the explicit process.exit(1) here unreachable and redundant.

Either remove the redundant exit or update the comment to clarify this is an intentional safety mechanism:

   if (!isLicenseValid()) {
     const error = getLicenseValidationError() || 'Invalid license';
     log.error(`[React on Rails Pro] ${error}`);
-    // License validation already calls process.exit(1) in handleInvalidLicense
-    // But we add this for safety in case the validator changes
-    process.exit(1);
+    // handleInvalidLicense in the validator already exits; this is unreachable
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd83e3c and 9650099.

⛔ Files ignored due to path filters (5)
  • react_on_rails_pro/Gemfile.lock is excluded by !**/*.lock
  • react_on_rails_pro/spec/dummy/Gemfile.lock is excluded by !**/*.lock
  • react_on_rails_pro/spec/dummy/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • react_on_rails_pro/spec/execjs-compatible-dummy/Gemfile.lock is excluded by !**/*.lock
  • react_on_rails_pro/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (19)
  • lib/react_on_rails/helper.rb (1 hunks)
  • react_on_rails_pro/.gitignore (1 hunks)
  • react_on_rails_pro/CI_SETUP.md (1 hunks)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1 hunks)
  • react_on_rails_pro/package.json (2 hunks)
  • react_on_rails_pro/packages/node-renderer/src/master.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1 hunks)
  • react_on_rails_pro/rakelib/public_key_management.rake (1 hunks)
  • react_on_rails_pro/react_on_rails_pro.gemspec (2 hunks)
  • react_on_rails_pro/spec/dummy/.gitignore (1 hunks)
  • react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore (1 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (12)
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb
  • react_on_rails_pro/package.json
  • react_on_rails_pro/lib/react_on_rails_pro/license_public_key.rb
  • react_on_rails_pro/spec/execjs-compatible-dummy/.gitignore
  • react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts
  • react_on_rails_pro/lib/react_on_rails_pro.rb
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
  • lib/react_on_rails/helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb
  • react_on_rails_pro/spec/dummy/.gitignore
  • react_on_rails_pro/react_on_rails_pro.gemspec
🧰 Additional context used
📓 Path-based instructions (3)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/rakelib/public_key_management.rake
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/LICENSE_SETUP.md
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/src/master.ts
  • react_on_rails_pro/CI_SETUP.md
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/packages/node-renderer/src/master.ts
🧠 Learnings (1)
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
🧬 Code graph analysis (2)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (6-14)
react_on_rails_pro/packages/node-renderer/src/master.ts (2)
react_on_rails_pro/packages/node-renderer/src/shared/configBuilder.ts (1)
  • Config (25-83)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (2)
  • isLicenseValid (166-168)
  • getLicenseValidationError (174-176)
🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/CI_SETUP.md

215-215: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (9)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: claude-review
  • GitHub Check: markdown-link-check
  • GitHub Check: build
  • GitHub Check: build-and-test
🔇 Additional comments (2)
react_on_rails_pro/rakelib/public_key_management.rake (2)

18-111: LGTM! Comprehensive error handling and clear user feedback.

The update_public_key task handles network errors, JSON parsing errors, and general exceptions well. The support for multiple sources (production, local, custom) provides good flexibility for development and testing.


114-173: LGTM! Thorough verification with helpful diagnostics.

The verify_public_key task provides comprehensive verification including key size validation, file existence checks, and optional license validation. The error messages guide users toward resolution effectively.

Comment on lines 60 to 75
ruby_content = <<~RUBY.strip_heredoc
# frozen_string_literal: true
module ReactOnRailsPro
module LicensePublicKey
# ShakaCode's public key for React on Rails Pro license verification
# The private key corresponding to this public key is held by ShakaCode
# and is never committed to the repository
# Last updated: #{Time.now.utc.strftime("%Y-%m-%d %H:%M:%S UTC")}
# Source: #{api_url}
KEY = OpenSSL::PKey::RSA.new(<<~PEM.strip.strip_heredoc)
#{public_key.strip}
PEM
end
end
RUBY
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify ActiveSupport availability and remove redundant strip calls.

  1. The strip_heredoc method requires ActiveSupport, which should be available in a Rails environment. However, ensure the rake task loads Rails/ActiveSupport properly.
  2. Line 70 has redundant .strip calls: public_key.strip.strip_heredoc - the .strip_heredoc already strips the content.

Apply this diff to remove the redundant strip:

-            KEY = OpenSSL::PKey::RSA.new(<<~PEM.strip.strip_heredoc)
+            KEY = OpenSSL::PKey::RSA.new(<<~PEM.strip_heredoc)
               #{public_key.strip}
             PEM

Verify that ActiveSupport is loaded when the rake task runs:


🏁 Script executed:

#!/bin/bash
# Check if the rake task requires Rails/ActiveSupport
rg -n "require.*active_support" react_on_rails_pro/rakelib/
rg -n "Rails\." react_on_rails_pro/rakelib/public_key_management.rake

Length of output: 84


Add ActiveSupport require and remove redundant strip

  • Prepend to react_on_rails_pro/rakelib/public_key_management.rake:
    require "active_support/core_ext/string"
  • Update heredoc invocation:
    @@ react_on_rails_pro/rakelib/public_key_management.rake
    - KEY = OpenSSL::PKey::RSA.new(<<~PEM.strip.strip_heredoc)
    + KEY = OpenSSL::PKey::RSA.new(<<~PEM.strip_heredoc)
       #{public_key.strip}
       PEM
🤖 Prompt for AI Agents
In react_on_rails_pro/rakelib/public_key_management.rake around lines 60 to 75,
add require "active_support/core_ext/string" at the top of the file (before
using strip_heredoc) and simplify the heredoc by removing the redundant call to
strip on the embedded public_key; keep strip_heredoc on the outer heredoc only
and pass the raw public_key interpolation (e.g., #{public_key}) so the resulting
Ruby constant uses ActiveSupport's strip_heredoc correctly.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (1)
react_on_rails_pro/LICENSE_SETUP.md (1)

119-123: Fix Ruby API reference in verification snippet

Use the actual helper and namespace.

Apply:

 rails console
-> ReactOnRails::Utils.react_on_rails_pro?
+# Use the Utils API provided by the Pro gem
+> ReactOnRailsPro::Utils.licence_valid?
 # Should return: true

Based on learnings

🧹 Nitpick comments (2)
react_on_rails_pro/LICENSE_SETUP.md (1)

60-71: Add file permission guidance for the license key

Prevent world-readable license files in repos/containers.

Apply:

 echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." > config/react_on_rails_pro_license.key
+
+# Restrict permissions (recommended)
+chmod 600 config/react_on_rails_pro_license.key
react_on_rails_pro/CI_SETUP.md (1)

371-371: Fix bare URL (MD034)

Use Markdown link syntax.

- - License expired (get a new FREE license at https://shakacode.com/react-on-rails-pro)
+ - License expired (get a new FREE license at [shakacode.com/react-on-rails-pro](https://shakacode.com/react-on-rails-pro))

As per coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9650099 and b11a3d9.

📒 Files selected for processing (7)
  • lib/react_on_rails/pro_utils.rb (1 hunks)
  • lib/react_on_rails/utils.rb (1 hunks)
  • react_on_rails_pro/CI_SETUP.md (1 hunks)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (10 hunks)
  • spec/dummy/spec/system/integration_spec.rb (1 hunks)
  • spec/react_on_rails/react_component/render_options_spec.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • lib/react_on_rails/pro_utils.rb
  • spec/react_on_rails/react_component/render_options_spec.rb
  • lib/react_on_rails/utils.rb
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
  • spec/dummy/spec/system/integration_spec.rb
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/LICENSE_SETUP.md
  • react_on_rails_pro/CI_SETUP.md
🧠 Learnings (4)
📓 Common learnings
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • lib/react_on_rails/pro_utils.rb
  • spec/react_on_rails/react_component/render_options_spec.rb
  • lib/react_on_rails/utils.rb
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
  • spec/dummy/spec/system/integration_spec.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • lib/react_on_rails/pro_utils.rb
  • spec/react_on_rails/react_component/render_options_spec.rb
  • lib/react_on_rails/utils.rb
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.

Applied to files:

  • lib/react_on_rails/utils.rb
🧬 Code graph analysis (1)
lib/react_on_rails/pro_utils.rb (1)
lib/react_on_rails/utils.rb (1)
  • react_on_rails_pro? (237-241)
🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/CI_SETUP.md

217-217: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: build
  • GitHub Check: claude-review
  • GitHub Check: build-and-test
🔇 Additional comments (12)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (8)

24-26: LGTM!

The test setup correctly stubs the renamed method react_on_rails_pro? to return true, maintaining the same test behavior as before.


388-433: LGTM!

Test scenarios correctly updated to stub react_on_rails_pro? instead of the previous method name. The test coverage for both licensed and unlicensed scenarios remains comprehensive.


435-450: LGTM!

Pro license validation test correctly updated to use the new method name.


482-492: LGTM!

Test properly stubs the new method and verifies the Pro licensing badge behavior.


494-506: LGTM!

Test correctly verifies behavior when Pro is installed and licensed.


540-554: LGTM!

Redux store test properly updated to use the new Pro check method.


556-564: LGTM!

Test correctly verifies that the Pro badge is not shown when license is invalid.


566-576: LGTM!

Test correctly verifies Pro licensing behavior with the updated method stub.

spec/react_on_rails/react_component/render_options_spec.rb (1)

23-25: LGTM!

Test setup correctly updated to stub the renamed method, maintaining proper test isolation for Pro feature testing.

spec/dummy/spec/system/integration_spec.rb (1)

89-99: LGTM!

Shared context correctly updated to use the new method name, maintaining consistent Pro feature enablement across integration tests.

lib/react_on_rails/utils.rb (1)

231-241: Ignore license validation concern in react_on_rails_pro? license is always validated at startup via the ReactOnRailsPro engine initializer, so this helper cannot return true for an unlicensed installation.

Likely an incorrect or invalid review comment.

lib/react_on_rails/pro_utils.rb (1)

8-11: Cannot confirm support_pro_features? enforces licensing
I couldn’t locate the react_on_rails_pro? implementation to verify it checks license validity. Ensure support_pro_features? either validates the license (e.g. via ReactOnRailsPro::LicenseValidator) or update the @return doc to “installed” only.

…pdate related tests for improved clarity and consistency
… production environments and update related tests for clarity and coverage
Copy link
Contributor

@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: 0

🧹 Nitpick comments (7)
react_on_rails_pro/LICENSE_SETUP.md (1)

240-240: Consider formatting the URL as a markdown link.

The bare URL can be formatted as a markdown link for consistency with other URLs in the document.

Apply this diff:

-3. **Email**: [email protected]
+3. **Email**: [[email protected]](mailto:[email protected])
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (3)

36-37: Consider reusing license_data to avoid potential double decoding.

The validate_license method calls load_and_decode_license directly, but license_data also calls the same method with memoization. If a caller invokes both validate! and license_data, the license may be decoded twice.

Apply this diff to use the memoized accessor:

       def validate_license
-        license = load_and_decode_license
+        license = license_data

Based on past review comments.


47-51: Consider adding clock tolerance for expiry validation.

The expiry check current_time > exp_time doesn't account for clock skew between systems. In distributed environments with clock drift, this can cause false rejections of valid licenses.

Add a clock tolerance constant and adjust the comparison:

       # Grace period: 1 month (in seconds)
       GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60
+      # Clock tolerance: allow 60 seconds of clock skew
+      CLOCK_TOLERANCE_SECONDS = 60

       def validate_license
         license = load_and_decode_license

         # ... exp field check ...

         # Check expiry with grace period for production
         current_time = Time.now.to_i
         exp_time = license["exp"]

-        if current_time > exp_time
+        if current_time > exp_time + CLOCK_TOLERANCE_SECONDS

This prevents short clock drift from triggering false expiration errors.

Based on past review comments.


130-132: Use config_path variable in error message for consistency.

The error message hardcodes the path string instead of using the config_path variable defined on line 127. Using the variable ensures the error message stays in sync if the path changes.

Apply this diff:

         @validation_error = "No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable " \
-                            "or create config/react_on_rails_pro_license.key file. " \
+                            "or create #{config_path} file. " \
                             "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"

Based on past review comments.

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (3)

221-226: Add error handling to prevent unhandled exceptions.

getLicenseData() calls loadAndDecodeLicense() which can throw JWT verification errors or file system errors. If called before validateLicense(), this could result in an unhandled exception.

Wrap the call in a try-catch block:

 export function getLicenseData(): LicenseData | undefined {
   if (!cachedLicenseData) {
-    loadAndDecodeLicense();
+    try {
+      loadAndDecodeLicense();
+    } catch (error) {
+      // Return undefined on error; validation state tracked separately
+      console.error(`[React on Rails Pro] Error loading license data: ${(error as Error).message}`);
+      return undefined;
+    }
   }
   return cachedLicenseData;
 }

Based on past review comments.


119-130: Consider adding runtime validation of decoded JWT payload.

The result of jwt.verify() is cast to LicenseData without runtime validation. While signature verification ensures the token is authentic, it doesn't guarantee the payload contains expected fields with correct types.

Add basic runtime validation before the cast:

   const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
     algorithms: ['RS256'],
     ignoreExpiration: true,
   }) as LicenseData;
+
+  // Validate decoded payload structure
+  if (typeof decoded !== 'object' || decoded === null || typeof decoded.exp !== 'number') {
+    throw new Error('Invalid license payload structure: missing or invalid exp field');
+  }

   cachedLicenseData = decoded;
   return decoded;

Based on past review comments.


151-156: Consider adding clock tolerance to manual expiry check.

While ignoreExpiration: true allows custom validation logic, the manual expiry check at line 156 (currentTime > expTime) could still benefit from clock tolerance to handle minor clock skew between systems.

Add a clock tolerance constant:

 // Grace period: 1 month (in seconds)
 const GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60;
+// Clock tolerance: allow 60 seconds of clock skew
+const CLOCK_TOLERANCE_SECONDS = 60;

 // ... in performValidation ...
     const currentTime = Date.now() / 1000;
     const expTime = license.exp;

-    if (currentTime > expTime) {
+    if (currentTime > expTime + CLOCK_TOLERANCE_SECONDS) {

This prevents false rejections due to minor clock drift, which is especially important in distributed systems.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9626ff2 and 05b8615.

📒 Files selected for processing (5)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
  • react_on_rails_pro/LICENSE_SETUP.md
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
🧠 Learnings (1)
📚 Learning: 2025-02-13T14:29:49.267Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: spec/react_on_rails/utils_spec.rb:218-218
Timestamp: 2025-02-13T14:29:49.267Z
Learning: In RSpec tests, prefer using local variables over constants within test blocks to avoid constant redefinition warnings and maintain better test isolation.

Applied to files:

  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
🧬 Code graph analysis (2)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (11-19)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (5)
  • public_key (136-138)
  • reset! (19-23)
  • validate! (6-154)
  • validate! (13-17)
  • license_data (25-27)
🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/LICENSE_SETUP.md

240-240: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (8)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: markdown-link-check
  • GitHub Check: build
  • GitHub Check: claude-review
  • GitHub Check: build-and-test
🔇 Additional comments (4)
react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1)

1-267: Excellent test coverage!

The test suite comprehensively covers all critical scenarios:

  • Valid license with caching behavior
  • Expired licenses with environment-specific grace period handling
  • Missing required fields (exp)
  • Invalid signatures
  • Missing license with proper error messages
  • Config file loading
  • Public API (license_data, validation_error, reset)

The tests properly use RSpec idioms and ensure test isolation through before/after blocks.

react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1)

108-110: Parameter is well-documented.

The third parameter to JWT.decode is properly explained by the inline comment on lines 108-109. The code clearly states that this must always be true for security, which addresses the concern from past review comments.

react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)

119-127: Excellent security practices in JWT verification.

The implementation correctly:

  • Enforces RS256 algorithm exclusively to prevent "alg=none" attacks
  • Includes clear comments about algorithm whitelist risks
  • Uses manual expiration handling with grace period logic
  • Properly documents security considerations

This demonstrates strong understanding of JWT security best practices.

Based on learnings about jsonwebtoken security requirements.

react_on_rails_pro/LICENSE_SETUP.md (1)

134-137: Documentation is correct
The method react_on_rails_pro? is defined on ReactOnRails::Utils, so the namespace and name in the docs are accurate.

Copy link
Contributor

@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 (3)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)

154-167: Consider using path interpolation in error message.

The implementation correctly checks ENV first, then the config file. However, the error message on lines 163-165 hardcodes the path string instead of interpolating the config_path variable.

Apply this diff for better maintainability:

       config_path = Rails.root.join("config", "react_on_rails_pro_license.key")
       return File.read(config_path).strip if config_path.exist?
 
       @validation_error = "No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable " \
-                          "or create config/react_on_rails_pro_license.key file. " \
+                          "or create #{config_path} file. " \
                           "Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro"

This ensures the path in the error message stays synchronized if the path logic changes.

Based on past review comments.


72-112: Clock skew can cause false license rejections.

The expiry check on line 86 (current_time > exp_time) doesn't account for clock skew between systems. In production environments with multiple servers or slight clock drift, valid licenses could be incorrectly rejected.

Consider adding a small clock tolerance (e.g., 60 seconds):

       # Check expiry with grace period for production
+      # Clock tolerance in seconds (allow 60 seconds of clock skew)
+      CLOCK_TOLERANCE = 60
+
       current_time = Time.now.to_i
       exp_time = license["exp"]
       grace_days = nil
 
-      if current_time > exp_time
+      if current_time > (exp_time + CLOCK_TOLERANCE)
         days_expired = ((current_time - exp_time) / (24 * 60 * 60)).to_i

Based on past review comments.

react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1)

13-22: Critical: Missing skip guards for asset precompilation and RSpec tests.

Per previous review comments, this initializer is missing essential guards to skip license validation during asset precompilation and RSpec test runs. Without these guards:

  1. License validation will fail during assets:precompile when the license file may not be available
  2. RSpec tests that use stub keys will be disrupted

Apply this diff to restore the necessary skip guards:

     initializer "react_on_rails_pro.validate_license" do
       # Use after_initialize to ensure Rails.logger is available
       config.after_initialize do
+        # Skip license validation during asset precompilation
+        next if defined?(Rake) && (Rake.application.top_level_tasks.include?("assets:precompile") rescue false)
+
+        # Skip license validation when running RSpec tests
+        # Tests mock the license validation with stub keys
+        next if defined?(RSpec)
+
         Rails.logger.info "[React on Rails Pro] Validating license..."
 
         ReactOnRailsPro::LicenseValidator.validated_license_data!

Based on past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05b8615 and 278b8dc.

📒 Files selected for processing (5)
  • lib/react_on_rails/helper.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb (2 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • lib/react_on_rails/helper.rb
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
  • react_on_rails_pro/lib/react_on_rails_pro/engine.rb
🧠 Learnings (1)
📚 Learning: 2025-04-09T12:56:10.756Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1696
File: node_package/src/RSCPayloadContainer.ts:0-0
Timestamp: 2025-04-09T12:56:10.756Z
Learning: In the react_on_rails codebase, RSC payloads are already stringified using `JSON.stringify()` before being processed by the `escapeScript` function, which handles escaping of special characters. The function only needs to handle specific HTML markers like comments and closing script tags.

Applied to files:

  • lib/react_on_rails/helper.rb
🧬 Code graph analysis (3)
lib/react_on_rails/helper.rb (2)
lib/react_on_rails/utils.rb (1)
  • react_on_rails_pro? (236-244)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
  • pro_attribution_comment (170-184)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)
  • grace_days_remaining (54-60)
  • evaluation? (46-50)
react_on_rails_pro/lib/react_on_rails_pro/engine.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)
  • validated_license_data! (6-187)
  • validated_license_data! (12-37)
⏰ 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). (6)
  • GitHub Check: examples (3.4, latest)
  • GitHub Check: build
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: claude-review
  • GitHub Check: build-and-test
🔇 Additional comments (8)
lib/react_on_rails/helper.rb (2)

616-630: LGTM!

The modification cleanly integrates the attribution comment before the Rails context script tag. The separation of attribution logic into a dedicated helper method improves maintainability.


632-640: LGTM!

The attribution comment method correctly delegates to the Pro version when active and provides an appropriate fallback for the open source version.

react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)

168-184: LGTM!

The Pro attribution comment generation correctly handles different license states (grace period, evaluation, paid) and produces informative HTML comments. The implementation properly delegates to LicenseValidator methods.

react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (5)

12-37: LGTM!

The license validation method implements proper caching, comprehensive error handling, and clear separation of concerns between loading/decoding and validation logic.


39-60: LGTM!

The helper methods (reset!, evaluation?, grace_days_remaining) are well-implemented with proper caching behavior and clear logic.


114-131: LGTM!

The grace period helper methods are clearly implemented with straightforward logic for production checks, grace period validation, and remaining days calculation.


133-152: Excellent security-conscious implementation!

The JWT decoding correctly enforces signature verification with RS256, and the comprehensive comments warn about critical security vulnerabilities (algorithm bypassing, signature verification). The manual expiration handling is appropriate for the custom grace period logic.


169-186: LGTM!

The helper methods correctly delegate to the public key module, provide consistent error handling with logging, and conditionally log license metadata.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
lib/react_on_rails/utils.rb (1)

236-244: Fix memoization for the gem unavailability case.

The return false statement on line 240 exits the method before the assignment to @react_on_rails_pro completes, preventing memoization when the gem is unavailable. This causes gem_available?("react_on_rails_pro") to be checked on every call when the gem is not installed.

Apply this diff to fix the memoization:

 def self.react_on_rails_pro?
   return @react_on_rails_pro if defined?(@react_on_rails_pro)

-  @react_on_rails_pro = begin
-    return false unless gem_available?("react_on_rails_pro")
-
-    ReactOnRailsPro::Utils.validated_license_data!.present?
-  end
+  @react_on_rails_pro = if gem_available?("react_on_rails_pro")
+    ReactOnRailsPro::Utils.validated_license_data!.present?
+  else
+    false
+  end
 end

This ensures @react_on_rails_pro is set to false and memoized when the gem is unavailable, eliminating redundant checks on subsequent calls.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 278b8dc and 5555b37.

📒 Files selected for processing (2)
  • lib/react_on_rails/utils.rb (1 hunks)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • lib/react_on_rails/utils.rb
🧠 Learnings (3)
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • lib/react_on_rails/utils.rb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • lib/react_on_rails/utils.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.

Applied to files:

  • lib/react_on_rails/utils.rb
🧬 Code graph analysis (1)
lib/react_on_rails/utils.rb (2)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
  • validated_license_data! (23-25)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (2)
  • validated_license_data! (6-187)
  • validated_license_data! (12-37)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: build
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
  • GitHub Check: build-and-test

Copy link
Contributor

@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: 0

🧹 Nitpick comments (2)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (2)

223-225: Optional: Remove unnecessary Rails.env stubbing

The pro_attribution_comment implementation (as shown in the relevant code snippets) doesn't reference Rails.env, making this stub unnecessary. Consider removing it unless it's defensive for planned changes.

 describe ".pro_attribution_comment" do
-  before do
-    allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
-  end
-
   context "when license is valid and not in grace period" do

256-259: Consider enhancing pluralization in the implementation

The test correctly verifies that when grace_days_remaining returns 1, the output is "1 days remaining". However, this is grammatically incorrect—it should be "1 day remaining" (singular).

While the test accurately reflects current behavior, consider enhancing the implementation to pluralize correctly:

In react_on_rails_pro/lib/react_on_rails_pro/utils.rb, apply this enhancement:

def self.pro_attribution_comment
  base = "Powered by React on Rails Pro (c) ShakaCode"

  grace_days = ReactOnRailsPro::LicenseValidator.grace_days_remaining
  comment = if grace_days
              day_word = grace_days == 1 ? "day" : "days"
              "#{base} | Licensed (Expired - Grace Period: #{grace_days} #{day_word} remaining)"
            elsif ReactOnRailsPro::LicenseValidator.evaluation?
              "#{base} | Evaluation License"
            else
              "#{base} | Licensed"
            end

  "<!-- #{comment} -->"
end

Then update the test expectation on line 259:

-                   "Licensed (Expired - Grace Period: 1 days remaining) -->"
+                   "Licensed (Expired - Grace Period: 1 day remaining) -->"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5555b37 and 58d680e.

📒 Files selected for processing (2)
  • react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (5 hunks)
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb
🧬 Code graph analysis (1)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (3)
lib/react_on_rails/utils.rb (1)
  • rsc_bundle_js_file_path (149-154)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (1)
  • pro_attribution_comment (646-646)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
  • pro_attribution_comment (170-184)
⏰ 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). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (2)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (2)

5-5: LGTM: RuboCop module length configuration

The disable/enable comments appropriately handle the module length as the test suite grows with new coverage.

Also applies to: 290-290


222-287: Good test coverage for pro_attribution_comment

The test suite comprehensively covers all license state scenarios: valid, grace period (multiple cases), evaluation, and edge cases. The mocking strategy correctly isolates the unit under test.

Minor observations:

  • Line 256: The description "returns attribution comment with singular day" could be clearer—it tests the behavior for one day remaining (count=1), not whether the grammar uses singular "day". The implementation currently always outputs "days" (plural) regardless of count.
  • Consider adding a test for nil license (if that's a valid state) to ensure proper error handling or default behavior.

def response; end
end

describe ReactOnRailsProHelper, type: :helper do
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The type: :helper metadata was removed by RuboCop (RSpecRails/InferredSpecType cop). RSpec Rails automatically infers the spec type from the file path (spec/helpers/), so the explicit type metadata is redundant and was removed.

Copy link
Contributor

@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

🧹 Nitpick comments (1)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (1)

641-641: Consider extracting attribution comment strings to constants or let blocks.

The attribution comment strings are repeated throughout the test file (e.g., "<!-- Powered by React on Rails Pro (c) ShakaCode | Licensed -->"). Extracting these to shared constants or let blocks would improve maintainability and reduce duplication.

Example:

let(:pro_attribution_comment) { "<!-- Powered by React on Rails Pro (c) ShakaCode | Licensed -->" }
let(:oss_attribution_comment) { "<!-- Powered by React on Rails (c) ShakaCode | Open Source -->" }

Then reference them throughout:

allow(utils_module).to receive(:pro_attribution_comment).and_return(pro_attribution_comment)
expect(result).to include(pro_attribution_comment)

Also applies to: 673-673, 690-690, 712-712, 728-728, 750-750, 775-775, 797-797, 813-813

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58d680e and cf975c7.

📒 Files selected for processing (2)
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb (2 hunks)
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (11 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
  • react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
🧠 Learnings (3)
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation is handled in deeper level calls of the React on Rails Pro codebase, so it doesn't need to be validated again in the `rsc_payload_react_component` helper method.

Applied to files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
🧬 Code graph analysis (2)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (2)
lib/react_on_rails/helper.rb (3)
  • react_component (58-95)
  • redux_store (257-272)
  • react_component_hash (212-242)
lib/react_on_rails/react_component/render_options.rb (1)
  • props (30-32)
react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb (2)
react_on_rails_pro/spec/react_on_rails_pro/support/mock_stream_helper.rb (1)
  • clear_stream_mocks (8-10)
lib/react_on_rails/helper.rb (1)
  • stream_react_component (128-136)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: claude-review
  • GitHub Check: build
  • GitHub Check: build-and-test
🔇 Additional comments (5)
react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb (2)

16-16: Verify removal of type: :helper is intentional.

The type: :helper metadata has been removed from the describe block. While RSpec 3+ typically infers the type from the spec's file location, this removal might affect any type-specific behavior or inclusions if they were relied upon.

Please confirm this change is intentional and that no type-specific RSpec behavior is needed here.


728-737: LGTM!

The test cases correctly verify that the Pro attribution comment is present in the rendered output and appears exactly once. The use of include for presence and scan(...).length for counting occurrences is appropriate.

spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (3)

24-28: LGTM! Mock setup aligns with new Pro API predicates.

The updated mocks correctly reflect the API changes from react_on_rails_pro_licence_valid? to react_on_rails_pro?, and add the new predicates react_on_rails_pro_version and rsc_support_enabled? as expected.


637-676: LGTM! Comprehensive test coverage for attribution comment helper.

The test suite properly stubs the ReactOnRailsPro modules using stub_const and validates both the Pro and open source attribution comment scenarios. The verification of method calls and return values is thorough.


394-394: LGTM! Consistent API migration across all test contexts.

All references correctly updated from react_on_rails_pro_licence_valid? to react_on_rails_pro?, maintaining consistency with the new Pro API predicates throughout the test suite.

Also applies to: 410-410, 426-426, 442-442, 488-488, 501-501, 546-546, 562-562, 573-573

Comment on lines +711 to +726
before do
@rorp_rendering_fibers = []
ReactOnRailsPro::Request.instance_variable_set(:@connection, nil)
original_httpx_plugin = HTTPX.method(:plugin)
allow(HTTPX).to receive(:plugin) do |*args|
original_httpx_plugin.call(:mock_stream).plugin(*args)
end
clear_stream_mocks

mock_streaming_response(%r{http://localhost:3800/bundles/[a-f0-9]{32}-test/render/[a-f0-9]{32}}, 200,
count: 1) do |yielder|
chunks.each do |chunk|
yielder.call("#{chunk.to_json}\n")
end
end
end
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Refactor to reduce code duplication.

The setup in this before block duplicates logic from the mock_request_and_response helper method (lines 330-347). Both perform similar operations: resetting the connection, mocking HTTPX.plugin, clearing stream mocks, and setting up mock streaming responses.

Consider extracting the common setup into a shared helper method or reusing mock_request_and_response with appropriate parameters:

     before do
       @rorp_rendering_fibers = []
-      ReactOnRailsPro::Request.instance_variable_set(:@connection, nil)
-      original_httpx_plugin = HTTPX.method(:plugin)
-      allow(HTTPX).to receive(:plugin) do |*args|
-        original_httpx_plugin.call(:mock_stream).plugin(*args)
-      end
-      clear_stream_mocks
-
-      mock_streaming_response(%r{http://localhost:3800/bundles/[a-f0-9]{32}-test/render/[a-f0-9]{32}}, 200,
-                              count: 1) do |yielder|
-        chunks.each do |chunk|
-          yielder.call("#{chunk.to_json}\n")
-        end
-      end
+      mock_request_and_response(chunks, count: 1)
     end

This reduces duplication and centralizes the mocking setup logic.

📝 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
before do
@rorp_rendering_fibers = []
ReactOnRailsPro::Request.instance_variable_set(:@connection, nil)
original_httpx_plugin = HTTPX.method(:plugin)
allow(HTTPX).to receive(:plugin) do |*args|
original_httpx_plugin.call(:mock_stream).plugin(*args)
end
clear_stream_mocks
mock_streaming_response(%r{http://localhost:3800/bundles/[a-f0-9]{32}-test/render/[a-f0-9]{32}}, 200,
count: 1) do |yielder|
chunks.each do |chunk|
yielder.call("#{chunk.to_json}\n")
end
end
end
before do
@rorp_rendering_fibers = []
mock_request_and_response(chunks, count: 1)
end
🤖 Prompt for AI Agents
In react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb
around lines 711 to 726, the before block duplicates setup already implemented
in mock_request_and_response (lines ~330-347); extract the repeated steps
(resetting ReactOnRailsPro::Request connection, mocking HTTPX.plugin, calling
clear_stream_mocks, and registering the streaming response) into a shared helper
or call mock_request_and_response with the appropriate args, keeping the
@rorp_rendering_fibers = [] initialization in place; replace the duplicated
block with a single call to that helper, passing the regex, status, count and
chunks so the streaming mock is configured centrally.

Copy link
Contributor

@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 (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1)

81-105: Add clock-skew tolerance to expiry check.

current_time > exp_time will reject valid licenses when app servers and signing host differ by a few seconds. Add a small buffer (e.g., 300 s) before failing so minor drift does not take production down.

-        current_time = Time.now.to_i
-        exp_time = license["exp"]
+        current_time = Time.now.to_i
+        exp_time = license["exp"]
+        expiry_with_tolerance = exp_time + CLOCK_TOLERANCE_SECONDS
         grace_days = nil
 
-        if current_time > exp_time
+        if current_time > expiry_with_tolerance

…and define the constant near GRACE_PERIOD_SECONDS:

-    GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60
+    GRACE_PERIOD_SECONDS = 30 * 24 * 60 * 60
+    CLOCK_TOLERANCE_SECONDS = 300
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf975c7 and 8b49a7a.

📒 Files selected for processing (2)
  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
  • react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb
🧬 Code graph analysis (2)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
  • validated_license_data! (23-25)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (3)
lib/react_on_rails/utils.rb (1)
  • rsc_bundle_js_file_path (149-154)
react_on_rails_pro/lib/react_on_rails_pro/configuration.rb (1)
  • configuration (9-34)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
  • pro_attribution_comment (170-184)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: claude-review
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: build
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: build-and-test

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (1)

692-709: Consider removing redundant react_on_rails_pro? stub in Pro contexts.

Multiple "Pro installed" contexts (lines 694, 732, 779, 817) explicitly set allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true), but this is already configured in the global before block (line 25). While not harmful, removing these redundant stubs would reduce duplication and rely on the global setup:

       context "when React on Rails Pro is installed" do
         before do
-          allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true)
           allow(ReactOnRailsPro::Utils).to receive(:pro_attribution_comment)
             .and_return("<!-- Powered by React on Rails Pro (c) ShakaCode | Licensed -->")
         end

Apply this pattern to similar contexts at lines 732, 779, and 817.

Also applies to: 730-747, 777-794, 815-847

Also applies to: 730-747, 777-794, 815-847, 849-871

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b63a704 and b9c0c5b.

📒 Files selected for processing (1)
  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (13 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
🧠 Learnings (2)
📚 Learning: 2025-02-18T13:08:01.477Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1644
File: lib/react_on_rails/helper.rb:190-197
Timestamp: 2025-02-18T13:08:01.477Z
Learning: RSC support validation in React on Rails Pro is handled through a chain of validations:
1. Pro version check in `run_stream_inside_fiber`
2. RSC support check during pack generation via `ReactOnRailsPro.configuration.enable_rsc_support`
3. RSC support validation during component registration
This makes additional validation in the helper methods unnecessary.

Applied to files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • spec/dummy/spec/helpers/react_on_rails_helper_spec.rb
🧬 Code graph analysis (1)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (2)
lib/react_on_rails/helper.rb (3)
  • react_component (58-95)
  • redux_store (257-272)
  • react_component_hash (212-242)
lib/react_on_rails/react_component/render_options.rb (2)
  • props (30-32)
  • include (8-175)
⏰ 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). (9)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: claude-review
  • GitHub Check: build-and-test
🔇 Additional comments (3)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (3)

30-40: LGTM! Global stub setup correctly addresses previous NameError issue.

The global stub_const setup for ReactOnRailsPro modules properly prevents NameError when tests reference ReactOnRailsPro::Utils.pro_attribution_comment. This addresses the concern from the previous review. The approach of defining the stub in the global before block (since react_on_rails_pro? is true by default) ensures all tests can safely reference Pro modules.

Based on past review comments (commit b63a704 addressed the missing stub_const issue).


648-682: Comprehensive test coverage for the attribution comment helper.

The tests properly verify both Pro and OSS attribution comment generation, including verification that ReactOnRailsPro::Utils.pro_attribution_comment is called when Pro is installed. The test structure is clear and properly isolated.


684-873: Excellent test coverage for attribution comment behavior.

The tests comprehensively verify that:

  • Attribution comments appear in react_component, redux_store, and react_component_hash outputs
  • Both Pro and OSS variants are tested
  • Comments appear exactly once per page, even with multiple helper calls
  • Mixed helper usage (component + store) maintains single attribution

The use of scan to count comment occurrences is appropriate for verifying the "only once" requirement.

Copy link
Contributor

@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

🧹 Nitpick comments (2)
spec/dummy/spec/system/integration_spec.rb (2)

99-106: Simplify constant stubbing; avoid clobbering parent module

Stubbing the parent constant is unnecessary and can shadow other nested constants (e.g., VERSION). Stub just the nested Utils, and use replace/transfer flags to be safe if it already exists.

-    pro_module = Module.new
     utils_module = Module.new do
       def self.pro_attribution_comment
         "<!-- Powered by React on Rails Pro (c) ShakaCode | Licensed -->"
       end
     end
-    stub_const("ReactOnRailsPro", pro_module)
-    stub_const("ReactOnRailsPro::Utils", utils_module)
+    stub_const(
+      "ReactOnRailsPro::Utils",
+      utils_module,
+      transfer_nested_constants: true,
+      replace: true
+    )

97-104: Tighten comment wording to reflect scope

“This for all tests” and “by default” is misleading; this applies only within this shared_context.

-    # Stub ReactOnRailsPro::Utils.pro_attribution_comment for all tests
-    # since react_on_rails_pro? is set to true by default
+    # Stub ReactOnRailsPro::Utils.pro_attribution_comment for tests in this context
+    # since react_on_rails_pro? is stubbed to true here
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9c0c5b and 125b364.

📒 Files selected for processing (1)
  • spec/dummy/spec/system/integration_spec.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • spec/dummy/spec/system/integration_spec.rb
🧠 Learnings (1)
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • spec/dummy/spec/system/integration_spec.rb
🧬 Code graph analysis (1)
spec/dummy/spec/system/integration_spec.rb (1)
spec/dummy/spec/helpers/react_on_rails_helper_spec.rb (1)
  • pro_attribution_comment (34-36)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: claude-review
  • GitHub Check: markdown-link-check
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: build
  • GitHub Check: build-and-test

Copy link
Contributor

@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: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 125b364 and 895d92a.

📒 Files selected for processing (4)
  • CHANGELOG.md (2 hunks)
  • react_on_rails_pro/CHANGELOG.md (1 hunks)
  • react_on_rails_pro/CI_SETUP.md (1 hunks)
  • react_on_rails_pro/LICENSE_SETUP.md (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/CHANGELOG.md
  • CHANGELOG.md
  • react_on_rails_pro/CI_SETUP.md
  • react_on_rails_pro/LICENSE_SETUP.md
🧠 Learnings (1)
📚 Learning: 2025-09-15T21:24:48.207Z
Learnt from: AbanoubGhadban
PR: shakacode/react_on_rails#1781
File: node_package/src/ClientSideRenderer.ts:82-95
Timestamp: 2025-09-15T21:24:48.207Z
Learning: In React on Rails, the force_load feature includes both explicit `data-force-load="true"` usage and the ability to hydrate components during the page loading state (`document.readyState === 'loading'`). Both capabilities require a Pro license, so the condition `!railsContext.rorPro && (isComponentForceLoaded || document.readyState === 'loading')` correctly gates both scenarios.

Applied to files:

  • CHANGELOG.md
🪛 LanguageTool
react_on_rails_pro/CI_SETUP.md

[grammar] ~19-~19: There might be a mistake here.
Context: ...## Option 1: Use a Team Member's License - Any developer's FREE license works for C...

(QB_NEW_EN)


[grammar] ~24-~24: There might be a mistake here.
Context: ... Option 2: Create a Dedicated CI License - Register with [email protected] or si...

(QB_NEW_EN)


[grammar] ~38-~38: There might be a mistake here.
Context: ... Click "New repository secret" 4. Name: REACT_ON_RAILS_PRO_LICENSE 5. Value: Your complete JWT license token (...

(QB_NEW_EN)


[grammar] ~87-~87: There might be a mistake here.
Context: ...riables 3. Click "Add variable" 4. Key: REACT_ON_RAILS_PRO_LICENSE 5. Value: Your license token 6. ✅ Check "Pr...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...RO_LICENSE` 5. Value: Your license token 6. ✅ Check "Protect variable" (optional) 7....

(QB_NEW_EN)


[grammar] ~123-~123: There might be a mistake here.
Context: ...ick "Add Environment Variable" 4. Name: REACT_ON_RAILS_PRO_LICENSE 5. Value: Your license token **Step 2: Use...

(QB_NEW_EN)


[grammar] ~179-~179: There might be a mistake here.
Context: ...ttings → Environment Variables 3. Name: REACT_ON_RAILS_PRO_LICENSE 4. Value: Your license token 5. ✅ Check "Di...

(QB_NEW_EN)


[grammar] ~180-~180: There might be a mistake here.
Context: ...RO_LICENSE` 4. Value: Your license token 5. ✅ Check "Display value in build log": **...

(QB_NEW_EN)


[grammar] ~219-~219: There might be a mistake here.
Context: ...ecret text 4. Secret: Your license token 5. ID: REACT_ON_RAILS_PRO_LICENSE 6. Desc...

(QB_NEW_EN)


[grammar] ~220-~220: There might be a mistake here.
Context: ...xt 4. Secret: Your license token 5. ID: REACT_ON_RAILS_PRO_LICENSE 6. Description: "React on Rails Pro License...

(QB_NEW_EN)


[grammar] ~270-~270: There might be a mistake here.
Context: ...RO_LICENSE` 4. Value: Your license token 5. ✅ Check "Secured" (recommended) **Step ...

(QB_NEW_EN)


[grammar] ~370-~370: There might be a mistake here.
Context: ...oad['exp']).to_s " ``` Common issues: - License not set in CI environment variab...

(QB_NEW_EN)


[grammar] ~387-~387: There might be a mistake here.
Context: ...: "No license found" in CI Checklist: - ✅ License added to CI environment variab...

(QB_NEW_EN)


[grammar] ~388-~388: There might be a mistake here.
Context: ...icense added to CI environment variables - ✅ Variable name is exactly `REACT_ON_RAI...

(QB_NEW_EN)


[grammar] ~390-~390: There might be a mistake here.
Context: ...icense value is complete (not truncated) - ✅ License is accessible in the job/step ...

(QB_NEW_EN)


[grammar] ~405-~405: There might be a mistake here.
Context: ...rror: "License has expired" Solution: 1. Get a new FREE 3-month license from [sha...

(QB_NEW_EN)


[grammar] ~412-~412: There might be a mistake here.
Context: ...Locally But Fail in CI Common causes: - License not set in CI environment - Wron...

(QB_NEW_EN)


[grammar] ~413-~413: There might be a mistake here.
Context: ...s:** - License not set in CI environment - Wrong variable name - License truncated ...

(QB_NEW_EN)


[grammar] ~414-~414: There might be a mistake here.
Context: ... in CI environment - Wrong variable name - License truncated when copying **Soluti...

(QB_NEW_EN)


[grammar] ~417-~417: There might be a mistake here.
Context: ...se truncated when copying Solution: Compare local and CI environments: ```b...

(QB_NEW_EN)


[grammar] ~452-~452: There might be a mistake here.
Context: ...luation purposes - renew every 3 months) - Staging: Can use FREE evaluation licen...

(QB_NEW_EN)


[grammar] ~453-~453: There might be a mistake here.
Context: ...r non-production testing or paid license - Production: Paid license (required - f...

(QB_NEW_EN)


[grammar] ~462-~462: There might be a mistake here.
Context: ...ar reminder**: 2 weeks before expiration 2. CI notification: Tests will fail when ...

(QB_NEW_EN)


[grammar] ~463-~463: There might be a mistake here.
Context: ...fication**: Tests will fail when expired 3. Email: We'll send renewal reminders #...

(QB_NEW_EN)


[grammar] ~470-~470: There might be a mistake here.
Context: ...rails-pro) 2. Log in with your email 3. Get new FREE license (or upgrade to paid) 4...

(QB_NEW_EN)


[grammar] ~478-~478: There might be a mistake here.
Context: ... - Documentation: LICENSE_SETUP.md - Get FREE License: [shakacode.com/react...

(QB_NEW_EN)


[grammar] ~479-~479: There might be a mistake here.
Context: ...nse**: shakacode.com/react-on-rails-pro - Email Support: [email protected] -...

(QB_NEW_EN)


[grammar] ~480-~480: There might be a mistake here.
Context: ...Email Support: [email protected] - CI Issues: Include your CI provider na...

(QB_NEW_EN)


[grammar] ~499-~499: There might be a mistake here.
Context: ...-license-token" ``` --- Quick Links: - 🎁 [Get FREE License](https://shakacode....

(QB_NEW_EN)


[grammar] ~500-~500: There might be a mistake here.
Context: ... Quick Links: - 🎁 Get FREE License - 📚 General Setup -...

(QB_NEW_EN)

react_on_rails_pro/LICENSE_SETUP.md

[grammar] ~22-~22: There might be a mistake here.
Context: ...e.** ## License Types ### Free License - Duration: 3 months - Usage: Person...

(QB_NEW_EN)


[grammar] ~23-~23: There might be a mistake here.
Context: ...## Free License - Duration: 3 months - Usage: Personal, educational, and eval...

(QB_NEW_EN)


[grammar] ~24-~24: There might be a mistake here.
Context: ...valuation, CI/CD) - NOT for production - Cost: FREE - just register with your e...

(QB_NEW_EN)


[grammar] ~25-~25: There might be a mistake here.
Context: ...**: FREE - just register with your email - Renewal: Get a new free license or upg...

(QB_NEW_EN)


[grammar] ~28-~28: There might be a mistake here.
Context: ...nse or upgrade to paid ### Paid License - Duration: 1 year (or longer) - **Usage...

(QB_NEW_EN)


[grammar] ~29-~29: There might be a mistake here.
Context: ...cense - Duration: 1 year (or longer) - Usage: Production deployment - *Cost...

(QB_NEW_EN)


[grammar] ~30-~30: There might be a mistake here.
Context: ...nger) - Usage: Production deployment - Cost: Subscription-based - Support...

(QB_NEW_EN)


[grammar] ~31-~31: There might be a mistake here.
Context: ...eployment - Cost: Subscription-based - Support: Includes professional support...

(QB_NEW_EN)


[grammar] ~81-~81: There might be a mistake here.
Context: ...d at multiple points: 1. Ruby Gem: When Rails application starts 2. **Node Rend...

(QB_NEW_EN)


[grammar] ~81-~81: There might be a mistake here.
Context: ...uby Gem**: When Rails application starts 2. Node Renderer: When the Node renderer ...

(QB_NEW_EN)


[grammar] ~82-~82: There might be a mistake here.
Context: ...*: When the Node renderer process starts 3. Browser Package: Trusts server-side va...

(QB_NEW_EN)


[grammar] ~103-~103: There might be a mistake here.
Context: ...mediately (helps catch expiration early) Important: The grace period is designe...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ployments time to renew, but you should: 1. Monitor your logs for license expiration...

(QB_NEW_EN)


[grammar] ~159-~159: There might be a mistake here.
Context: ... Error: "No license found" Solutions: 1. Verify environment variable: `echo $REAC...

(QB_NEW_EN)


[grammar] ~170-~170: There might be a mistake here.
Context: ...ust be complete JWT token) Solutions: 1. Ensure you copied the complete license (...

(QB_NEW_EN)


[grammar] ~177-~177: There might be a mistake here.
Context: ...: "License has expired" What happens: - Development/Test/CI: Application fails...

(QB_NEW_EN)


[grammar] ~181-~181: There might be a mistake here.
Context: ... logs, then fails to start Solutions: 1. Free License: Get a new 3-month FREE l...

(QB_NEW_EN)


[grammar] ~187-~187: There might be a mistake here.
Context: ... You have time to renew, but don't wait! - The warning shows how many days remain -...

(QB_NEW_EN)


[grammar] ~225-~225: There might be a mistake here.
Context: ...ation**: No internet connection required - Public key verification: Uses embedded...

(QB_NEW_EN)


[grammar] ~226-~226: There might be a mistake here.
Context: ...fication**: Uses embedded RSA public key - Tamper-proof: Any modification invalid...

(QB_NEW_EN)


[grammar] ~227-~227: There might be a mistake here.
Context: ...y modification invalidates the signature - No tracking: License validation happen...

(QB_NEW_EN)


[grammar] ~240-~240: There might be a mistake here.
Context: ...nse at shakacode.com/react-on-rails-pro 2. Documentation: Check [CI_SETUP.md](./C...

(QB_NEW_EN)


[grammar] ~242-~242: There might be a mistake here.
Context: ...tion 3. Email: [email protected] 4. License Management: [shakacode.com/rea...

(QB_NEW_EN)


[grammar] ~247-~247: There might be a mistake here.
Context: ...nfig/react_on_rails_pro_license.keyto.gitignore` 2. ✅ **Use environment variables in product...

(QB_NEW_EN)


[grammar] ~265-~265: There might be a mistake here.
Context: ... happens when my free license expires?** A: Get a new 3-month FREE license, or up...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.18.1)
react_on_rails_pro/CI_SETUP.md

33-33: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


41-41: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


82-82: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


92-92: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


118-118: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


126-126: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


175-175: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


183-183: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


214-214: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


223-223: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


265-265: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


273-273: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


304-304: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


310-310: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


373-373: Bare URL used

(MD034, no-bare-urls)


480-480: Bare URL used

(MD034, no-bare-urls)

react_on_rails_pro/LICENSE_SETUP.md

242-242: Bare URL used

(MD034, no-bare-urls)

⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: build
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: build-and-test

Comment on lines +344 to +351

License validation happens automatically when Rails starts.

✅ **If your CI tests run, your license is valid**
❌ **If license is invalid, Rails fails to start immediately**

**No verification step needed** - the application won't start without a valid license.

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify what actually happens on invalid licenses

This section promises CI will fail automatically, but in non-production environments the validator only logs warnings. Please update the doc so teams know tests may still run with an expired/missing license.

🤖 Prompt for AI Agents
react_on_rails_pro/CI_SETUP.md around lines 344 to 351: the current text asserts
that CI tests will fail on invalid licenses because Rails refuses to start, but
in non-production environments the license validator only emits warnings and
tests can still pass; update the wording to state this distinction (production:
Rails fails to start; development/test: validator logs warnings and tests may
run), and add a short recommendation to include an explicit license validation
step in CI (for example running the license check task or booting in production
mode) so teams know how to ensure CI actually fails on invalid or expired
licenses.

Copy link
Contributor

@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 (4)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (4)

84-108: ESLint disable is a symptom of the process.exit issue.

The eslint-disable-next-line consistent-return comment (line 84) exists because handleInvalidLicense() never returns due to process.exit(). Once the critical process.exit issue is addressed, this ESLint disable can be removed.

After refactoring handleInvalidLicense() to not exit, update the function to explicitly return or throw:

-// eslint-disable-next-line consistent-return
 function loadLicenseString(): string {
   // ... existing code ...
   
-  handleInvalidLicense(errorMsg);
+  handleInvalidLicense(errorMsg);
+  throw new Error(errorMsg); // Make control flow explicit
 }

190-227: Another ESLint disable due to process.exit.

Similar to loadLicenseString(), the eslint-disable-next-line consistent-return (line 190) exists because error paths call handleInvalidLicense() which exits the process.

The error handling logic itself is good—it distinguishes JsonWebTokenError from other errors and provides helpful messages. However, this is another symptom of the critical process.exit issue already flagged.

After removing process.exit() from handleInvalidLicense(), update error handling:

-// eslint-disable-next-line consistent-return
 export function getValidatedLicenseData(): LicenseData {
   // ... existing code ...
   } catch (error: unknown) {
     if (error instanceof Error && error.name === 'JsonWebTokenError') {
       // ... build errorMsg ...
       handleInvalidLicense(errorMsg);
+      throw new Error(errorMsg);
     } else if (error instanceof Error) {
       // ... build errorMsg ...
       handleInvalidLicense(errorMsg);
+      throw error;
     } else {
       // ... build errorMsg ...
       handleInvalidLicense(errorMsg);
+      throw new Error('Unknown validation error');
     }
   }
 }

32-37: Critical: Process exit still prevents testing and violates separation of concerns.

This is the same issue raised in previous reviews. The handleInvalidLicense() function unconditionally calls process.exit(1), which:

  • Makes the validator untestable (tests crash instead of verifying error handling)
  • Violates separation of concerns (libraries shouldn't control process lifecycle)
  • Contradicts the PR description stating "master.ts handles environment-specific behavior"

The validator should return or throw validation state, letting master.ts decide whether to exit based on environment.

Refactor to remove process control:

-function handleInvalidLicense(message: string): never {
+function handleInvalidLicense(message: string): void {
   const fullMessage = `[React on Rails Pro] ${message}`;
   console.error(fullMessage);
-  // Validation errors should prevent the application from starting
-  process.exit(1);
+  // Let the caller decide whether to exit based on environment
 }

Then update master.ts to handle exit logic:

const licenseData = getValidatedLicenseData();
if (!licenseData && process.env.NODE_ENV === 'production') {
  process.exit(1);
}

117-128: Unsafe type cast without runtime validation.

The result of jwt.verify() is cast to LicenseData without validating the payload structure (line 125). While signature verification ensures authenticity, it doesn't guarantee the expected fields exist with correct types.

Add runtime validation before the cast:

   const decoded = jwt.verify(licenseString, PUBLIC_KEY, {
     // ... existing options ...
   }) as LicenseData;
+
+  // Validate payload structure
+  if (
+    typeof decoded !== 'object' ||
+    decoded === null ||
+    typeof decoded.exp !== 'number'
+  ) {
+    throw new Error('Invalid license payload: missing or malformed required fields');
+  }

-  return decoded;
+  return decoded as LicenseData;
 }
🧹 Nitpick comments (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)

43-63: Consider adding timestamp conversion comment.

The helper functions correctly implement grace period logic. However, the / 1000 conversion from milliseconds to seconds appears multiple times. Consider adding a comment explaining this, as suggested in past reviews.

Add a comment at the first occurrence:

 function isWithinGracePeriod(expTime: number): boolean {
+  // Date.now() returns milliseconds; convert to seconds for Unix timestamp comparison
   return Math.floor(Date.now() / 1000) <= expTime + GRACE_PERIOD_SECONDS;
 }

Alternatively, extract a helper function:

function getCurrentUnixTime(): number {
  return Math.floor(Date.now() / 1000);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 895d92a and d5b43a0.

📒 Files selected for processing (3)
  • react_on_rails_pro/packages/node-renderer/src/master.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1 hunks)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • react_on_rails_pro/packages/node-renderer/tests/licenseValidator.test.ts
  • react_on_rails_pro/packages/node-renderer/src/master.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx,css,scss,json,yml,yaml,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Prettier is the sole authority for formatting all non-Ruby files; never manually format them

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint for JS/TS code (lint via rake lint or yarn lint)

Files:

  • react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts
🧬 Code graph analysis (1)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (1)
react_on_rails_pro/packages/node-renderer/src/shared/licensePublicKey.ts (1)
  • PUBLIC_KEY (11-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). (8)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: claude-review
  • GitHub Check: build
  • GitHub Check: markdown-link-check
  • GitHub Check: build-and-test
🔇 Additional comments (4)
react_on_rails_pro/packages/node-renderer/src/shared/licenseValidator.ts (4)

6-19: License payload structure looks good.

The LicenseData interface correctly uses standard JWT claim names (sub, iat, exp, iss) and properly marks exp as required. The index signature allows flexibility for additional claims while maintaining type safety for known fields.


234-238: LGTM: Evaluation check is clear and safe.

The function correctly determines evaluation status by checking if the plan is "paid" or starts with "paid_". The fallback to an empty string (line 236) safely handles missing plan fields.


245-251: LGTM: Grace days retrieval is correct.

The function properly ensures validation has occurred before returning cached grace days. The caching strategy is consistent with the rest of the module.


256-259: LGTM: Reset function is necessary for testing.

This function correctly clears all cached state, which is essential for test isolation given the module-level caching strategy. The documentation clearly indicates its purpose.

Copy link
Contributor

@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 (2)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)

168-184: Fix grammatical error in attribution comment.

The attribution comment uses "day(s)" for all cases, which is grammatically incorrect when grace_days equals 1. The message should read "1 day remaining" (singular) instead of "1 day(s) remaining".

Apply this diff to handle singular/plural correctly:

     def self.pro_attribution_comment
       base = "Powered by React on Rails Pro (c) ShakaCode"
 
       # Check if in grace period
       grace_days = ReactOnRailsPro::LicenseValidator.grace_days_remaining
       comment = if grace_days
-                  "#{base} | Licensed (Expired - Grace Period: #{grace_days} day(s) remaining)"
+                  day_word = grace_days == 1 ? "day" : "days"
+                  "#{base} | Licensed (Expired - Grace Period: #{grace_days} #{day_word} remaining)"
                 elsif ReactOnRailsPro::LicenseValidator.evaluation?
                   "#{base} | Evaluation License"
                 else
                   "#{base} | Licensed"
                 end
 
       "<!-- #{comment} -->"
     end

Based on past review comments.

react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (1)

247-257: Update test expectation for singular grammar.

Once the implementation is updated to use "1 day" (singular) instead of "1 day(s)", this test expectation should also be updated to match.

Apply this diff after fixing the implementation:

         it "returns attribution comment with singular day" do
           result = described_class.pro_attribution_comment
           expected = "<!-- Powered by React on Rails Pro (c) ShakaCode | " \
-                     "Licensed (Expired - Grace Period: 1 day(s) remaining) -->"
+                     "Licensed (Expired - Grace Period: 1 day remaining) -->"
           expect(result).to eq(expected)
         end

Based on past review comments.

🧹 Nitpick comments (1)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (1)

25-25: LGTM!

The consolidation of multiple allow(...).to receive(...) calls into single receive_messages calls is a clean refactor that improves test readability.

Also applies to: 44-45, 81-87

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5b43a0 and b083a65.

📒 Files selected for processing (2)
  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb (2 hunks)
  • react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}

📄 CodeRabbit inference engine (CLAUDE.md)

{Gemfile,Rakefile,config.ru,**/*.{rb,rake,gemspec,ru}}: All Ruby code must pass RuboCop with zero offenses before commit/push
RuboCop is the sole authority for Ruby file formatting; never manually format Ruby files

Files:

  • react_on_rails_pro/lib/react_on_rails_pro/utils.rb
  • react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb
🧬 Code graph analysis (2)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb (4)
  • validated_license_data! (6-187)
  • validated_license_data! (15-40)
  • grace_days_remaining (57-63)
  • evaluation? (49-53)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (2)
lib/react_on_rails/utils.rb (1)
  • rsc_bundle_js_file_path (149-154)
react_on_rails_pro/lib/react_on_rails_pro/utils.rb (1)
  • pro_attribution_comment (170-184)
⏰ 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). (10)
  • GitHub Check: dummy-app-integration-tests (3.2, 20)
  • GitHub Check: dummy-app-integration-tests (3.4, 22)
  • GitHub Check: rspec-package-tests (3.4, latest)
  • GitHub Check: rspec-package-tests (3.2, minimum)
  • GitHub Check: rspec-package-tests (3.2, latest)
  • GitHub Check: rspec-package-tests (3.4, minimum)
  • GitHub Check: build
  • GitHub Check: markdown-link-check
  • GitHub Check: claude-review
  • GitHub Check: build-and-test
🔇 Additional comments (1)
react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb (1)

222-283: LGTM!

The test coverage for pro_attribution_comment is comprehensive, covering all the different license states: valid license, grace period (multiple days), grace period (1 day), evaluation license, and expired grace period (0 days). The use of receive_messages for stubbing is clean and follows Ruby best practices.

Comment on lines +19 to +25
# Validates the license and raises an exception if invalid.
#
# @return [Boolean] true if license is valid
# @raise [ReactOnRailsPro::Error] if license is invalid
def self.validated_license_data!
LicenseValidator.validated_license_data!
end
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the return type documentation.

The documentation states the method returns [Boolean] true, but it actually returns a Hash containing the license data (delegated from LicenseValidator.validated_license_data!).

Apply this diff to correct the documentation:

-    # Validates the license and raises an exception if invalid.
-    #
-    # @return [Boolean] true if license is valid
-    # @raise [ReactOnRailsPro::Error] if license is invalid
+    # Validates the license and returns the license data.
+    # Raises an exception if the license is invalid.
+    #
+    # @return [Hash] The validated license data
+    # @raise [ReactOnRailsPro::Error] if license is invalid
     def self.validated_license_data!
📝 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
# Validates the license and raises an exception if invalid.
#
# @return [Boolean] true if license is valid
# @raise [ReactOnRailsPro::Error] if license is invalid
def self.validated_license_data!
LicenseValidator.validated_license_data!
end
# Validates the license and returns the license data.
# Raises an exception if the license is invalid.
#
# @return [Hash] The validated license data
# @raise [ReactOnRailsPro::Error] if license is invalid
def self.validated_license_data!
LicenseValidator.validated_license_data!
end
🤖 Prompt for AI Agents
In react_on_rails_pro/lib/react_on_rails_pro/utils.rb around lines 19 to 25, the
YARD comment incorrectly claims this method returns [Boolean] true while it
actually returns the Hash from LicenseValidator.validated_license_data!; update
the @return line to reflect the correct type and description (e.g. @return
[Hash] the validated license data) and keep the @raise line as-is to indicate
the method still raises ReactOnRailsPro::Error on invalid license.

Copy link

claude bot commented Oct 15, 2025

PR Review: Offline JWT-Based License Validation System

I've completed a comprehensive review of this PR. Overall, this is a well-implemented, security-conscious license validation system. The code quality is excellent with strong test coverage and clear documentation.


✅ Strengths

1. Excellent Security Implementation

The JWT validation correctly implements critical security best practices:

Ruby (license_validator.rb:136-151):

  • ✅ Signature verification explicitly enabled (true)
  • ✅ Algorithm hardcoded to RS256 (prevents algorithm substitution attacks)
  • ✅ Manual expiration handling (allows grace period logic)
  • ✅ Excellent inline comments warning about security implications

TypeScript (licenseValidator.ts:117-125):

  • ✅ Algorithm whitelist contains only RS256 (prevents "none" algorithm attack)
  • ✅ Clear comments about HMAC downgrade risks
  • ✅ Proper signature verification

Public Key Management:

  • ✅ Public keys are identical across Ruby and TypeScript (critical for consistency)
  • ✅ Keys are properly embedded in code (offline validation)
  • ✅ Update mechanism documented via rake task

2. Robust Architecture

  • Offline-first: No network dependencies for validation
  • Multi-layer validation: Ruby gem, Node renderer, and browser (trusts server)
  • Graceful degradation: Production gets 1-month grace period, dev/test fail fast
  • Caching strategy: Results cached after first validation (performance optimization)
  • Clear separation: Validation logic cleanly separated from business logic

3. Comprehensive Test Coverage

Ruby Tests (license_validator_spec.rb):

  • ✅ Valid license scenarios
  • ✅ Expired licenses (dev vs production behavior)
  • ✅ Grace period logic (within/outside 30 days)
  • ✅ Missing required fields, invalid signatures, missing licenses
  • ✅ File vs environment variable sources
  • ✅ Caching behavior and reset functionality

TypeScript Tests (licenseValidator.test.ts):

  • ✅ Valid license validation
  • ✅ Expired license handling
  • ✅ Missing exp field, invalid signature, missing license
  • ✅ Caching behavior and evaluation vs paid license detection

4. Excellent Documentation

  • LICENSE_SETUP.md: Comprehensive 272-line guide
  • CI_SETUP.md: Detailed 502-line guide for multiple CI providers
  • CHANGELOG entries: Clear communication of changes
  • Inline code comments: Security warnings where needed

🔍 Issues & Recommendations

Critical Issues

None! The implementation is solid.

High Priority (Should Fix)

1. Grace Period Calculation Edge Case

Files: license_validator.rb:125-131, licenseValidator.ts:59-63

Issue: Using integer division means "0.9 days remaining" shows as "0 days remaining", which might confuse users who see "0 days" but still have hours left.

Recommendation: Use ceiling to avoid "0 days" when time remains, or show hours if < 1 day.

Medium Priority (Consider)

2. Attribution Comment Exposes License Status

Files: helper.rb:633, utils.rb:170-184

Issue: The HTML comment reveals license status including grace period to anyone viewing page source. Consider hiding detailed status in production while keeping it visible in development for debugging.

3. TypeScript: Production Grace Period Tests Missing

Ruby tests thoroughly cover production grace period behavior, but TypeScript tests don't have explicit production environment tests with grace period validation.

Low Priority (Nice to Have)

  • Improve error message actionability with step-by-step instructions
  • Document grace period constant synchronization requirement between Ruby and TypeScript
  • Consider more explicit caching with instance_variable_defined? instead of defined?

🔒 Security Analysis

Strengths:

  1. Algorithm Hardening: Both implementations correctly enforce RS256 only
  2. Signature Verification: Never disabled, always validated
  3. No Signature Bypass: Manual expiration handling prevents JWT library auto-bypass
  4. Public Key Security: Properly managed, updateable, never exposes private key
  5. Input Validation: Checks required fields (exp) before processing
  6. Secure Defaults: Fails closed (exit on error) rather than open
  7. No Timing Attacks: Uses cryptographic library verification (constant-time)

No Critical Security Issues Found 🎉


📊 Performance & Test Quality

Performance: No Issues 👍

  • ✅ Caching makes RSA verification a one-time cost
  • ✅ Offline validation (no network calls)
  • ✅ Startup validation (not per-request)

Test Quality: Excellent ⭐⭐⭐⭐⭐

  • ✅ Comprehensive coverage of happy paths and error paths
  • ✅ Uses generated test keys (not hardcoded)
  • ✅ Properly mocks external dependencies
  • ✅ Clear test names and good organization

✅ Final Verdict

Approval: YES

This is a high-quality implementation with strong security awareness, comprehensive testing, excellent documentation, and thoughtful architecture.

Required Before Merge:

  1. ✅ Run bundle exec rubocop and fix any violations (per CLAUDE.md)
  2. ✅ Verify all files end with newlines (per CLAUDE.md)
  3. ✅ Replace placeholder public keys with real ShakaCode keys (noted in PR description)

Recommended Before Merge:

  1. Address grace period calculation edge case
  2. Consider attribution comment exposure in production
  3. Add TypeScript production environment tests

Checklist Summary

  • ✅ Code quality: Excellent
  • ✅ Security: No issues found
  • ✅ Performance: No issues
  • ✅ Test coverage: Comprehensive
  • ✅ Documentation: Excellent

Great work, @AbanoubGhadban! This is a solid foundation for the Pro license system. 🎉

Review generated by Claude Code 🤖

@AbanoubGhadban AbanoubGhadban merged commit b3ce154 into master Oct 15, 2025
18 of 19 checks passed
@AbanoubGhadban AbanoubGhadban deleted the feature/pro-license-validation branch October 15, 2025 14:39
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