Skip to content

Remove Pro license key-file fallback and add migration guidance#2454

Merged
ihabadham merged 12 commits intomasterfrom
ihabadham/chore/remove-license-key-file-option-ror
Feb 24, 2026
Merged

Remove Pro license key-file fallback and add migration guidance#2454
ihabadham merged 12 commits intomasterfrom
ihabadham/chore/remove-license-key-file-option-ror

Conversation

@ihabadham
Copy link
Collaborator

@ihabadham ihabadham commented Feb 19, 2026

Summary

  • remove React on Rails Pro runtime fallback to config/react_on_rails_pro_license.key (Ruby + Node validators now env-var only)
  • update Pro docs to make env-var setup explicit and add migration notes for legacy file users
  • add runtime migration warning when legacy key file is detected while env var is missing
  • add/adjust specs for validator + engine warning behavior
  • remove the non-compliant unreleased changelog bullet
  • add TODO comment to remove legacy migration warning path after 16.5.0 stable

Why

Align runtime behavior and documentation, and reduce upgrade surprises for existing installs that still rely on the legacy key-file path.

Testing

  • pnpm exec jest tests/licenseValidator.test.ts --runInBand
  • bundle exec rubocop --force-exclusion --display-cop-names -- react_on_rails_pro/lib/react_on_rails_pro/engine.rb react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb
  • pre-commit and pre-push lefthook checks passed
  • Manual repro: with only config/react_on_rails_pro_license.key and no env var, Ruby and Node both report missing

Notes

  • Could not run react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb end-to-end in this environment due missing PostgreSQL headers (libpq-fe.h) for the Pro bundle context.

Summary by CodeRabbit

  • Breaking Changes

    • License must be provided via REACT_ON_RAILS_PRO_LICENSE environment variable; local license file support removed. Migrate any existing file-based keys to the environment variable.
  • Documentation

    • Setup, installation, and update docs updated with migration steps and environment-variable guidance.
  • Behavior

    • If an old/local license file is present, a migration notice is logged (warn in production, info otherwise).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Consolidates license loading to use only the REACT_ON_RAILS_PRO_LICENSE environment variable, removes file-system-based loading and fs/path usage in the Node renderer, adds legacy config-file detection and migration notices in the Rails engine, and updates tests and documentation accordingly.

Changes

Cohort / File(s) Summary
Node renderer license loading
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts, packages/react-on-rails-pro-node-renderer/tests/licenseValidator.test.ts
Removed fs/path imports and config-file fallback; license is read exclusively from REACT_ON_RAILS_PRO_LICENSE. Tests no longer mock fs and were simplified to rely on ENV-based behavior.
Rails license validation
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb
load_license_string simplified to return the trimmed value of REACT_ON_RAILS_PRO_LICENSE or nil; file-based branches and related error handling removed.
Legacy-file migration detection
react_on_rails_pro/lib/react_on_rails_pro/engine.rb
Added private LEGACY_LICENSE_FILE constant plus helpers to detect the legacy file and log a migration notice (warn in production, info otherwise) when license status is missing.
CLI/status output
react_on_rails_pro/lib/react_on_rails_pro/license_task_formatter.rb
Removed instruction suggesting placement of a local key file from missing-license status output.
Rails tests
react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb, react_on_rails_pro/spec/react_on_rails_pro/license_validator_spec.rb
Added contexts testing legacy-file detection and environment-aware logging; removed tests for reading/unreadable legacy config file; added ENV whitespace/newline trimming tests.
Documentation & guides
react_on_rails_pro/CLAUDE.md, react_on_rails_pro/LICENSE_SETUP.md, react_on_rails_pro/docs/installation.md, react_on_rails_pro/docs/updating.md
Replaced file-based license setup with environment-variable-only guidance, added migration notes instructing moving tokens from config/react_on_rails_pro_license.key to REACT_ON_RAILS_PRO_LICENSE, and removed .gitignore/key-file instructions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped from file to env so free,
No hidden keys beneath a tree,
One token now, tidy and bright,
A migration nudge, gentle and light,
Hooray — simpler days in sight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: removing the license key-file fallback and adding migration guidance, which is the core focus across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ihabadham/chore/remove-license-key-file-option-ror

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.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 19, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.5 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.5 KB (0%)
react-on-rails/client bundled (brotli) 53.71 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.71 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.5 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.5 KB (0%)
react-on-rails-pro/client bundled (brotli) 54.67 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.67 KB (0%)
registerServerComponent/client bundled (gzip) 127.16 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.16 KB (0%)
registerServerComponent/client bundled (brotli) 61.54 KB (-0.08% 🔽)
registerServerComponent/client bundled (brotli) (time) 61.54 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 121.69 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 121.69 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.63 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.63 KB (0%)

@greptile-apps
Copy link

greptile-apps bot commented Feb 19, 2026

Greptile Summary

This PR removes the legacy file-based license configuration (config/react_on_rails_pro_license.key) and standardizes on environment variable only (REACT_ON_RAILS_PRO_LICENSE). Both Ruby and Node validators now exclusively read from the environment variable. Documentation has been updated throughout to reflect this change, and a runtime migration warning was added to alert users still using the legacy file approach.

Key Changes:

  • Ruby validator: removed file reading logic from load_license_string
  • Node validator: removed fs/path imports and file-based loading
  • Engine: added legacy_license_file_present? check and migration warning
  • Tests: removed file-based test scenarios and fs mocks
  • Docs: updated LICENSE_SETUP.md, installation.md, updating.md, and CLAUDE.md
  • Added TODO comment for removing migration warning after 16.5.0 stable

Issues Found:

  • Test expectations in engine_spec.rb appear to be incorrect (lines 62-65 and 192-195)

Confidence Score: 4/5

  • Safe to merge after fixing test expectations
  • Implementation is clean and well-tested, but test expectations need correction to match actual behavior (single logger call vs two separate calls)
  • react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb - fix test expectations for migration warnings

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts Removed file-based license loading, now env-var only; removed fs and path imports
react_on_rails_pro/LICENSE_SETUP.md Removed config file method, made env-var required, added migration note
react_on_rails_pro/docs/updating.md Added deprecation policy section and legacy key-file migration note
react_on_rails_pro/lib/react_on_rails_pro/engine.rb Added legacy license file detection and migration warning with TODO for removal
react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb Removed file-based license loading logic, now env-var only
react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb Added tests for legacy license file migration warnings in production and dev/test

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App Startup] --> B{License Status Check}
    B -->|Valid License| C[App Runs Normally]
    B -->|Missing License| D{Legacy File Exists?}
    D -->|Yes| E[Log Migration Warning]
    D -->|No| F[Log Missing License]
    E --> G{Environment}
    F --> G
    G -->|Production| H[Warn: License Violation]
    G -->|Dev/Test| I[Info: No License Required]
    H --> J[App Continues]
    I --> J
    B -->|Expired/Invalid| K[Log License Issue]
    K --> G
    
    style E fill:#ff9
    style H fill:#faa
    style I fill:#afa
Loading

Last reviewed commit: 4e09661

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

12 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Remove Pro License Key-File Fallback

Overall, the goal of this PR is sound — simplifying the license loading path to env-var-only and adding a migration warning is the right approach. The Ruby implementation in engine.rb and the documentation updates are clean. However, there are issues that need addressing before merging.


Critical: Test Expectations Are Wrong (engine_spec.rb)

The two new spec contexts (when legacy license file exists) have incorrect message expectations that will cause test failures.

The problem: log_legacy_license_migration_notice emits a single Rails.logger.warn (or info) call containing both "legacy license file" and "REACT_ON_RAILS_PRO_LICENSE" in one message. But the tests set up two separate receive expectations:

expect(mock_logger).to receive(:warn).with(/legacy license file/)       # expects call #1
expect(mock_logger).to receive(:warn).with(/REACT_ON_RAILS_PRO_LICENSE/) # expects call #2
described_class.log_license_status

What actually happens when log_license_status runs in the missing-license production context:

  • Call 1: Migration notice — "[React on Rails Pro] Detected legacy license file at …, but this file is no longer read. Move your token to REACT_ON_RAILS_PRO_LICENSE." — satisfies the first expectation (/legacy license file/).
  • Call 2: License issue — "[React on Rails Pro] No license found. Using React on Rails Pro…" — does not match /REACT_ON_RAILS_PRO_LICENSE/, so the second expectation is never satisfied.

Result: warn expected 1 time with arguments matching /REACT_ON_RAILS_PRO_LICENSE/ but received 0 times.

The same problem affects the development context with info calls.

Fix: Combine into a single expectation that matches the full migration notice, and use allow or receive (without with) for the other warn/info calls:

it "logs migration warning" do
  expect(mock_logger).to receive(:warn)
    .with(/Detected legacy license file.*REACT_ON_RAILS_PRO_LICENSE/m)
  allow(mock_logger).to receive(:warn)  # allow the license-issue warning
  described_class.log_license_status
end

Or, if you want to verify both messages separately, use ordered expectations and match each call's actual content.


Minor: Empty #### Pro Section in CHANGELOG

The diff adds a bare #### Pro heading under [Unreleased] with no content beneath it. If this is a placeholder, it should have at least a single line (e.g., a reference to this PR's change). Otherwise it should be omitted — an empty subsection will confuse changelog readers.


Minor: Node.js Side Has No Migration Warning

The Ruby Engine now logs a migration notice when the legacy key file is detected. The Node licenseValidator.ts was symmetrically simplified (file-reading code removed), but there is no equivalent Node-side warning. A user running the Node renderer in a setup where only the legacy file exists will just receive a silent 'missing' status from Node, while only the Ruby side explains why.

This is acceptable if the intent is to rely solely on the Ruby warning, but it's worth documenting explicitly (the current PR description implies both sides warn).


Nitpick: Redundant || undefined in TypeScript

return envLicense || undefined;

Since envLicense is already string | undefined (empty string is falsy), this is fine logically, but || undefined adds noise. envLicense being "" and returning undefined is the intended behavior — just worth a brief comment explaining the falsy-string edge case, since it's not obvious.


What's Good

  • The Ruby load_license_string simplification is clean and correct.
  • The legacy_license_file_present? detection logic and the log_legacy_license_migration_notice implementation are solid.
  • private_constant :LEGACY_LICENSE_FILE with the dated TODO comment is a good practice.
  • Removing the file-read fallback from license_validator.rb and its corresponding specs is done correctly.
  • Documentation updates across LICENSE_SETUP.md, installation.md, updating.md, and CLAUDE.md are clear and accurate.
  • The deprecation policy section added to updating.md is a useful governance addition.

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)
packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts (1)

55-55: getLicenseOrganization / getLicensePlan cache misses on undefined results — re-verifies JWT on every call.

The new comment at Line 55 emphasises "once per worker" deterministic caching, but cachedLicenseOrganization and cachedLicensePlan use undefined as both the uninitialized sentinel and the valid "not found" return value. When determineLicenseOrganization() / determineLicensePlan() return undefined (missing or invalid license), the check !== undefined is always false, so JWT verification runs on every invocation. getLicenseStatus() doesn't have this problem because its return type (LicenseStatus) is never undefined.

Consider using a dedicated "not yet computed" sentinel:

♻️ Suggested fix (sentinel pattern)
+const NOT_COMPUTED = Symbol('NOT_COMPUTED');
+
-let cachedLicenseOrganization: string | undefined;
-let cachedLicensePlan: ValidPlan | undefined;
+let cachedLicenseOrganization: string | undefined | typeof NOT_COMPUTED = NOT_COMPUTED;
+let cachedLicensePlan: ValidPlan | undefined | typeof NOT_COMPUTED = NOT_COMPUTED;
 export function getLicenseOrganization(): string | undefined {
-  if (cachedLicenseOrganization !== undefined) {
+  if (cachedLicenseOrganization !== NOT_COMPUTED) {
     return cachedLicenseOrganization;
   }
   cachedLicenseOrganization = determineLicenseOrganization();
   return cachedLicenseOrganization;
 }
 export function getLicensePlan(): ValidPlan | undefined {
-  if (cachedLicensePlan !== undefined) {
+  if (cachedLicensePlan !== NOT_COMPUTED) {
     return cachedLicensePlan;
   }
   cachedLicensePlan = determineLicensePlan();
   return cachedLicensePlan;
 }
 export function reset(): void {
   cachedLicenseStatus = undefined;
-  cachedLicenseOrganization = undefined;
-  cachedLicensePlan = undefined;
+  cachedLicenseOrganization = NOT_COMPUTED;
+  cachedLicensePlan = NOT_COMPUTED;
 }

Also applies to: 233-240, 271-278

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts` at
line 55, cachedLicenseOrganization and cachedLicensePlan use undefined as both
"not yet computed" and "computed-but-not-found", causing
determineLicenseOrganization()/determineLicensePlan() to re-run JWT verification
on every call; change the cache to use a dedicated uninitialized sentinel (e.g.,
a module-level constant like UNINITIALIZED) instead of undefined, initialize
cachedLicenseOrganization/cachedLicensePlan to that sentinel, update
getLicenseOrganization/getLicensePlan to check !== UNINITIALIZED before
returning cached values, and when computing assign either the actual value
(string) or undefined to the cache; apply the same sentinel change to the other
cache instances referenced around the other blocks (the ones flagged in the
comment) so computed-undefined is stored and not retriggered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Around line 30-31: Add an Unreleased → Pro changelog entry in CHANGELOG.md
noting the breaking change "Removing the legacy key-file fallback" under the Pro
section; format the entry exactly as "[PR
<number>](https://github.com/shakacode/react_on_rails/pull/<number>) by
[username](https://github.com/username)" (no “#” before the PR number) and
include the PR number and author for traceability.

In `@react_on_rails_pro/lib/react_on_rails_pro/license_validator.rb`:
- Around line 235-241: The load_license_string method returns the raw ENV value
which may include trailing whitespace/newlines causing token verification to
fail; update load_license_string to strip the fetched
ENV["REACT_ON_RAILS_PRO_LICENSE"] (e.g., call .strip) before returning and
return nil if the stripped string is empty so downstream verification uses a
clean token; refer to the load_license_string method when making this change.

---

Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/src/shared/licenseValidator.ts`:
- Line 55: cachedLicenseOrganization and cachedLicensePlan use undefined as both
"not yet computed" and "computed-but-not-found", causing
determineLicenseOrganization()/determineLicensePlan() to re-run JWT verification
on every call; change the cache to use a dedicated uninitialized sentinel (e.g.,
a module-level constant like UNINITIALIZED) instead of undefined, initialize
cachedLicenseOrganization/cachedLicensePlan to that sentinel, update
getLicenseOrganization/getLicensePlan to check !== UNINITIALIZED before
returning cached values, and when computing assign either the actual value
(string) or undefined to the cache; apply the same sentinel change to the other
cache instances referenced around the other blocks (the ones flagged in the
comment) so computed-undefined is stored and not retriggered.

@claude
Copy link

claude bot commented Feb 19, 2026

Review: Remove Pro license key-file fallback

The overall direction of this PR is good — removing the file-based fallback simplifies the license loading path and the migration warning is a thoughtful addition. A few issues worth addressing before merging:

Breaking change missing from CHANGELOG

The #### Pro section added under [Unreleased] is empty. Removing the config/react_on_rails_pro_license.key fallback is a breaking change for any user relying on that file. It needs a changelog entry, e.g.:

#### Pro

- **Breaking**: Removed file-based license key fallback (`config/react_on_rails_pro_license.key`). License must now be configured via the `REACT_ON_RAILS_PRO_LICENSE` environment variable. See the [migration guide](react_on_rails_pro/docs/updating.md) for details.

Node.js side has no migration warning

engine.rb logs a warning when the legacy key file is detected but no env var is set. The Node.js licenseValidator.ts has no equivalent. If a user runs only a Node renderer (no Ruby startup), they'll get a silent "missing" status with no hint about the renamed approach. This is an asymmetry worth noting, even if fixing it is deferred.

Minor: load_license_string redundant nil guard

ENV.fetch("REACT_ON_RAILS_PRO_LICENSE", nil) already returns nil when unset; license && in license if license && \!license.strip.empty? is harmless but redundant. Could simplify to license unless license.nil? || license.strip.empty? — though this is purely stylistic.

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.

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

63-67: Redundant allow stubs before have_received assertions.

let(:mock_logger) { instance_double(Logger, warn: nil, info: nil) } already installs allow-style stubs with call-tracking for both warn and info, so re-issuing allow(mock_logger).to receive(:warn) (line 63) and allow(mock_logger).to receive(:info) (line 194) before the assertions is redundant. have_received works on the existing stubs without the extra allow.

♻️ Proposed cleanup
         it "logs migration warning for env-var setup" do
-          allow(mock_logger).to receive(:warn)
           described_class.log_license_status
           expect(mock_logger).to have_received(:warn).with(/legacy license file/)
           expect(mock_logger).to have_received(:warn).with(/REACT_ON_RAILS_PRO_LICENSE/)
         end
         it "logs migration info for env-var setup" do
-          allow(mock_logger).to receive(:info)
           described_class.log_license_status
           expect(mock_logger).to have_received(:info).with(/legacy license file/)
           expect(mock_logger).to have_received(:info).with(/REACT_ON_RAILS_PRO_LICENSE/)
         end

Also applies to: 194-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb` around lines 63 -
67, Remove the redundant explicit stubs that re-call allow(mock_logger).to
receive(:warn) and allow(mock_logger).to receive(:info); the let-created
instance_double mock_logger (defined with warn: nil, info: nil) already provides
call-tracking for have_received, so delete those extra allow(...) lines and
leave the described_class.log_license_status invocation and the
expect(mock_logger).to have_received(:warn).with(...) / expect(mock_logger).to
have_received(:info).with(...) assertions as-is to rely on the original
instance_double stubs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@react_on_rails_pro/spec/react_on_rails_pro/engine_spec.rb`:
- Around line 63-67: Remove the redundant explicit stubs that re-call
allow(mock_logger).to receive(:warn) and allow(mock_logger).to receive(:info);
the let-created instance_double mock_logger (defined with warn: nil, info: nil)
already provides call-tracking for have_received, so delete those extra
allow(...) lines and leave the described_class.log_license_status invocation and
the expect(mock_logger).to have_received(:warn).with(...) /
expect(mock_logger).to have_received(:info).with(...) assertions as-is to rely
on the original instance_double stubs.

@claude
Copy link

claude bot commented Feb 19, 2026

Review: Remove Pro license key-file fallback

Overall this is a clean, well-scoped change with good rationale. The env-var-only approach is simpler, more secure, and consistent with modern deployment practices. The migration notice and deprecation policy doc are thoughtful additions.

Positive highlights

  • Removing filesystem-based license loading reduces attack surface (no symlink/path traversal risks on the license file path).
  • The double-checked locking pattern in LicenseValidator is preserved and correct.
  • Algorithm pinning (RS256) on JWT verification remains in both Ruby and Node validators — good.
  • The private_constant on LEGACY_LICENSE_FILE is appropriate.
  • The TODO with a concrete version + date target (16.5.0 / 2026-05-31) is better than an open-ended one.

Issues

1. Test coverage gap in the legacy-file warning contexts (engine_spec.rb)

The new nested contexts only assert the migration message was warned/info'd, but don't verify that the primary log_license_issue call (/No license found/) is also still emitted. Both assertions target patterns that match the single migration notice message — the second warn call from log_license_issue goes unchecked. If someone accidentally removed the log_license_issue call after the guard, these tests would still pass.

Suggested addition to each new context:

expect(mock_logger).to have_received(:warn).with(/No license found/)  # production context
# or
expect(mock_logger).to have_received(:info).with(/No license found/)  # dev context

2. Migration warning silently skipped for expired/invalid licenses

log_legacy_license_migration_notice is only triggered under the :missing branch. A user who placed the wrong token in the env var (so status is :invalid) while still having the legacy file present will not receive the migration notice. This may be intentional — they have the env var configured, just with a bad value — but it is worth an explicit comment in the code (or a broader guard) so the decision is clear to future maintainers.

3. Minor: redundant || undefined in TypeScript

return envLicense || undefined;

envLicense is already string | undefined. The || undefined correctly coerces the empty-string case to undefined, but the intent isn't obvious at a glance. A comment or a more explicit form (e.g., return envLicense || undefined /* coerce empty string */) would help readers understand why the original if (envLicense) { return envLicense; } pattern was simplified this way.

4. license_task_formatter.rb — missing migration hint

The now-removed line puts "or place a key file at config/react_on_rails_pro_license.key" was replaced with nothing. If a user runs the license Rake task and sees only No license found. Set REACT_ON_RAILS_PRO_LICENSE, they get no hint that they may have a legacy file that's being ignored. Consider adding a check there similar to the engine migration notice, or at minimum a note in the formatter output.

@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Remove Pro license key-file fallback

Overall this is a clean, well-structured change. The env-var-only approach is simpler and more secure, the migration notice in the engine is appropriately scoped by environment, and the Ruby/Node implementations are kept in sync. A few issues worth addressing before merge:

Bug: Stale Rails.root stub in license_task_formatter_spec.rb

license_task_formatter_spec.rb (not modified in this PR) still has:

let(:mock_root) { instance_double(Pathname, join: config_file_path) }
let(:config_file_path) { instance_double(Pathname, exist?: false) }
# ...
allow(Rails).to receive(:root).and_return(mock_root)

Since LicenseValidator no longer calls Rails.root at all, this stub and its related let declarations are now dead code. They should be removed alongside this PR's changes to keep the spec clean and avoid misleading future readers.

UX gap: rake verify_license task misses the migration hint

LicenseTaskFormatter now only prints:

No license found. Set REACT_ON_RAILS_PRO_LICENSE

But if a developer has the legacy config/react_on_rails_pro_license.key and runs rake react_on_rails_pro:verify_license, they will see that message without any indication that their existing key file is being ignored. The engine initializer does log a migration warning, but the rake task output is often the first place a developer looks when debugging license issues. Consider surfacing the same legacy-file hint in LicenseTaskFormatter (e.g., accepting an optional legacy_file_present: flag from the rake task caller).

Minor: redundant test assertions match the same warn call

In engine_spec.rb, for the legacy-file-present contexts:

expect(mock_logger).to have_received(:warn).with(/legacy license file/)
expect(mock_logger).to have_received(:warn).with(/REACT_ON_RAILS_PRO_LICENSE/)

Both patterns are matched by the single warn call from log_legacy_license_migration_notice. have_received(:warn).with checks that at least one call matched the pattern, so both assertions can pass against the same invocation. A single assertion with a combined pattern (e.g., /legacy license file.*REACT_ON_RAILS_PRO_LICENSE/m) or an exactly(1).times guard would more precisely express the intent and catch future regressions if one portion of the message is removed.

@ihabadham ihabadham force-pushed the ihabadham/chore/remove-license-key-file-option-ror branch from 17ab66e to 8668993 Compare February 19, 2026 22:42
@claude
Copy link

claude bot commented Feb 19, 2026

Review Notes

The overall approach is sound: dropping file-based license loading simplifies the codebase, reduces the attack surface, and the runtime migration notice is a good UX touch for users who haven't migrated yet. A few things worth addressing before merge:

Missing CHANGELOG entry

This PR introduces a breaking change for any users still relying on config/react_on_rails_pro_license.key. The PR description says it removes an existing changelog bullet as "non-compliant", but doesn't add a replacement entry. Given the project's documented practice (see the [Unreleased] section in CHANGELOG.md), a brief #### Breaking Changes or #### Changed (Pro) entry for 16.4.0 should document this removal and point readers to the migration note in docs/updating.md.

TODO without an issue reference (engine.rb:8)

# TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31).

Date-anchored TODOs tend to get missed. Linking to a GitHub issue (e.g., # TODO: Remove ... See #XXXX) would make follow-through easier to track.

Missing negative test: env var set + legacy file present

The new engine specs test that the migration notice fires when the file is detected and the env var is absent. But there's no test confirming the notice is suppressed when the env var is valid and the legacy file also exists — which is the "happy migration path" many users will hit. Without this test, a regression where the notice fires unconditionally would go undetected.

Spec assertions match a single warn call twice

In engine_spec.rb (production context), the new test does:

expect(mock_logger).to have_received(:warn).with(/legacy license file/)
expect(mock_logger).to have_received(:warn).with(/REACT_ON_RAILS_PRO_LICENSE/)

Both patterns are satisfied by the same single warn call (the migration notice message contains both substrings). This works correctly, but gives the appearance of verifying two distinct log lines. A single assertion with a combined regex — or a comment noting they target the same message — would make the test intent clearer.

TypeScript: return envLicense || undefined is benign but slightly surprising

const envLicense = process.env.REACT_ON_RAILS_PRO_LICENSE?.trim();
return envLicense || undefined;

envLicense is already string | undefined, so an empty string ("") after trim goes through || undefinedundefined. This is correct. The pattern is slightly unusual; return envLicense || undefined is equivalent to just return envLicense || undefined but could also be written as return envLicense ? envLicense : undefined to make the empty-string case more explicit. Minor style point, not a bug.

@claude
Copy link

claude bot commented Feb 20, 2026

Review Summary

Overall the PR is clean and well-structured. The approach of env-var-only validation (removing the file fallback) is the right direction, and the runtime migration warning with a TODO-with-date is good practice. A few items to address:

Must Fix

Missing CHANGELOG entry. This is a breaking change: users relying on config/react_on_rails_pro_license.key will silently get a missing license status after upgrading. The PR description says it removes a "non-compliant" unreleased bullet but never adds a compliant replacement. Per the project's conventions, a Pro ##### Breaking Changes entry is needed in the [Unreleased] section:

- **Removed legacy key-file license fallback**: `config/react_on_rails_pro_license.key` is no longer read.
  Move your token to the `REACT_ON_RAILS_PRO_LICENSE` environment variable.
  A migration warning is logged at startup when the legacy file is detected without the env var set.
  [PR 2454](https://github.com/shakacode/react_on_rails/pull/2454) by [ihabadham](https://github.com/ihabadham).

Migration hint missing from rake task output. The react_on_rails_pro:verify_license rake task prints its status directly to stdout via LicenseTaskFormatter, bypassing Rails.logger. When a user runs the task and the legacy file is present but the env var is not set, the stdout output just says "No license found. Set REACT_ON_RAILS_PRO_LICENSE" — with no mention of the legacy file. The engine-initializer migration warning only reaches the Rails log, which a CI pipeline checking rake exit codes may never surface. Consider echoing the migration hint from within print_status_line too (it has access to no file-system context currently — it could be passed in as a flag, or the rake task itself could check and prepend a line).

Already Flagged by Other Reviewers (Agree)

  • TypeScript caching sentinel (CodeRabbit): cachedLicenseOrganization and cachedLicensePlan use undefined as both the uninitialised sentinel and the "computed but not found" value, so JWT re-verification runs on every call when the license is missing/invalid. The sentinel-symbol fix CodeRabbit suggested is the right approach.
  • Redundant allow stubs in engine_spec (CodeRabbit): mock_logger is already fully stubbed by instance_double(Logger, warn: nil, info: nil), so the extra allow(mock_logger).to receive(:warn/info) before have_received assertions are noise.

@@ -42,7 +42,6 @@ def print_status_line(status)

puts ""
puts "No license found. Set REACT_ON_RAILS_PRO_LICENSE"
Copy link

Choose a reason for hiding this comment

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

The rake task outputs directly to stdout, bypassing Rails.logger. When a user runs rake react_on_rails_pro:verify_license with only the legacy key file present (no env var), the Rails log will contain the migration notice but this stdout output will just say "No license found. Set REACT_ON_RAILS_PRO_LICENSE" — giving the user no indication that the legacy file was detected but ignored.

Consider adding a migration hint here when the legacy file exists, for example:

def print_status_line(status)
  puts "Status:        #{status.to_s.upcase}"
  return unless status == :missing

  puts ""
  puts "No license found. Set REACT_ON_RAILS_PRO_LICENSE"
  # TODO: Remove after 16.5.0 stable
  legacy_file = Rails.root.join("config", "react_on_rails_pro_license.key")
  if legacy_file.exist?
    puts "NOTE: Detected legacy config/react_on_rails_pro_license.key — this file is no longer read."
    puts "      Move the token to REACT_ON_RAILS_PRO_LICENSE."
  end
end

Or pass the hint in from the rake task itself (keeping the formatter dependency-free).

@claude
Copy link

claude bot commented Feb 20, 2026

Code Review

Overall, this is a clean and well-reasoned PR. Removing the file-based license fallback and migrating to env-var-only is a good security improvement. The implementation is solid. A few observations below.

✅ What's done well

  • Correct caching fix: The UNINITIALIZED symbol sentinel properly fixes the pre-existing bug where a cached undefined org or plan couldn't be distinguished from "never computed." The previous \!== undefined guard would have silently re-decoded the JWT on every call when the field was absent.
  • Migration UX: Logging a migration notice at startup (only when the legacy file exists AND no env var is set) is well-targeted. The condition is correct — :missing means the env var is absent, so the notice only fires when the user actually needs it.
  • Thread-safety: The Ruby side correctly uses defined?(@ivar) + double-checked locking with LICENSE_MUTEX. The Node side correctly acknowledges that JS is single-threaded per-worker and redundant computation is acceptable.
  • JWT security posture: Algorithm is pinned to RS256, signature verification is never disabled — no regressions here.

⚠️ Issues to address

1. Inconsistent cache sentinel in TypeScript (licenseValidator.ts)

cachedLicenseStatus still uses undefined as its "not-yet-initialized" sentinel while cachedLicenseOrganization and cachedLicensePlan now use UNINITIALIZED. This is technically correct today (since LicenseStatus = 'valid' | 'expired' | 'invalid' | 'missing' never equals undefined), but it's an inconsistency that could silently break if the type ever changes, and it's not obvious at a glance why cachedLicenseStatus doesn't follow the same pattern as the other two. A short comment explaining the asymmetry would prevent confusion for future maintainers.

2. Misleading || undefined idiom in loadLicenseString (TypeScript)

return envLicense || undefined;

envLicense is already typed string | undefined via optional chaining. The || undefined is doing real work (converting an empty string to undefined) but looks like accidental noise because the type already allows undefined. A brief inline comment — or replacing with return envLicense || undefined; // treat empty string as missing — would help.

3. Redundant test expectations (engine_spec.rb)

Both these assertions match the same single logged message:

expect(mock_logger).to have_received(:warn).with(/legacy license file/)
expect(mock_logger).to have_received(:warn).with(/REACT_ON_RAILS_PRO_LICENSE/)

The migration notice is one string that contains both substrings. Having two expectations that resolve to the same call doesn't add coverage — it just makes it look like two separate log calls are expected. Either collapse to one assertion with a combined regex (/legacy license file.*REACT_ON_RAILS_PRO_LICENSE/) or add a comment clarifying it's the same call.

ℹ️ Minor notes

  • The TODO date (2026-05-31) is sensible given the 16.5.0 target window.
  • private_constant :LEGACY_LICENSE_FILE is good hygiene.
  • The new whitespace-trimming tests in license_validator_spec.rb (with surrounding whitespace, with trailing newline) are a welcome addition — these are exactly the edge cases that burn users in CI.
  • The mock_root.join stub uses a catch-all (instance_double(Pathname, join: config_file_path)) which would intercept any Rails.root.join call in the test, but since LicenseValidator#load_license_string no longer calls Rails.root.join, this doesn't cause any test pollution.

@ihabadham ihabadham added the needs-review No final review yet label Feb 20, 2026
@ihabadham ihabadham force-pushed the ihabadham/chore/remove-license-key-file-option-ror branch from 442de93 to e825c05 Compare February 24, 2026 15:35
@claude
Copy link

claude bot commented Feb 24, 2026

Review: Remove Pro license key-file fallback

The overall approach is clean and correct. Removing the file-based fallback simplifies the validator, the UNINITIALIZED sentinel fixes a real caching bug, and the migration warning is a good UX touch.

Issues

UX gap – migration notice silently skipped when env var is valid

The migration warning in engine.rb only fires when status == :missing. A user who correctly sets REACT_ON_RAILS_PRO_LICENSE but also still has the legacy config/react_on_rails_pro_license.key file on disk will never see any prompt to clean it up. That file contains a credential and will just sit there indefinitely. Consider logging a warning/info message in the :valid branch too when the legacy file is detected, so teams are prompted to delete it.

Caching inconsistency between cachedLicenseStatus and the other two caches

cachedLicenseStatus still uses undefined as its "not yet computed" sentinel, while cachedLicenseOrganization and cachedLicensePlan now use UNINITIALIZED. This is technically safe because LicenseStatus can never be undefined, but it is a reader trap: the next developer who adds a nullable return type to getLicenseStatus() will introduce the same bug this PR is fixing. A comment explaining why cachedLicenseStatus is exempt, or applying the sentinel consistently, would prevent that.

Missing TypeScript whitespace/trim tests

The Ruby spec adds explicit tests for surrounding whitespace and trailing newlines. The TypeScript loadLicenseString also trims via ?.trim(), but the TS test file does not have parallel coverage. If the trimming logic is ever changed on the TS side, there is nothing to catch it.

Minor nit

return envLicense || undefined; in loadLicenseString (TS) works correctly, but the intent is non-obvious. The ?.trim() already yields string | undefined; the || undefined is there only to coerce an empty string to undefined. A brief comment would make this immediately clear.

Positive notes

  • The UNINITIALIZED Symbol sentinel is the right fix for the caching bug.
  • Ruby load_license_string simplification to ENV.fetch(..., nil)&.strip.presence is clean and also fixes the pre-existing inconsistency where the env var path returned an unstripped value while the file path stripped it.
  • Test coverage for the new caching behavior (org absent, invalid plan) is well structured.
  • The TODO comment in engine.rb with a specific removal target date is good practice.

Users who migrated to REACT_ON_RAILS_PRO_LICENSE but left the old
config/react_on_rails_pro_license.key on disk were never prompted
to delete it. Now logs an info-level cleanup notice in that case.

Also adds TS whitespace/trim tests paralleling the Ruby coverage,
and a comment clarifying the || undefined idiom in loadLicenseString.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ihabadham
Copy link
Collaborator Author

Addressing the remaining items from the review above:

UX gap — migration notice skipped when env var is valid:
Fixed in 4c7aa53. engine.rb now logs an info-level cleanup notice (can be safely deleted) when the license is valid but the legacy file still exists. Engine spec updated to verify.

Missing TypeScript whitespace/trim tests:
Added in 4c7aa53 — three new tests: surrounding whitespace, trailing newline, and whitespace-only env var. Parallels the existing Ruby coverage.

@claude
Copy link

claude bot commented Feb 24, 2026

PR Review: Remove Pro license key-file fallback and add migration guidance

Overall this is a well-executed breaking change. The intent is clear, the logic is correct, and test coverage is solid. A few issues worth addressing before merge:

Critical / Correctness

None — the behavioral changes are correct.

Moderate Issues

1. Expired/invalid license + legacy file: no migration notice logged
In engine.rb, log_legacy_license_migration_notice is only triggered for :missing status. If a user has the legacy file AND has an expired or invalid env var, they see no migration hint. While less likely, this edge case could leave users confused ("I have the key file, why is my license invalid?"). Consider adding the migration check to :expired and :invalid branches, or at minimum document this gap.

2. Overlapping matchers in migration warning spec
In engine_spec.rb, the production missing+legacy test uses three separate have_received(:warn) assertions (/legacy license file/, /REACT_ON_RAILS_PRO_LICENSE/, /No license found/). The first two both match against the same single warn call (the migration message contains both substrings), giving the impression of verifying distinct messages when it doesn't. This doesn't cause test failures but is misleading. Consider using a single regex that matches the full migration message, or use exactly(2).times to assert the total number of warn calls.

Minor Issues

3. Cache sentinel asymmetry in licenseValidator.ts
cachedLicenseStatus uses undefined as its uninitialized sentinel (which happens to work since LicenseStatus never equals undefined), while cachedLicenseOrganization and cachedLicensePlan correctly use UNINITIALIZED. The UNINITIALIZED symbol was introduced specifically to fix the problem of caching a valid undefined result — but that same symbol pattern is not applied to cachedLicenseStatus. This is not a bug today, but it introduces a maintenance trap: if someone later adds undefined to the LicenseStatus union (e.g., to model "not yet checked"), the cache check will silently break. For consistency and future safety, cachedLicenseStatus should use UNINITIALIZED as well.

4. Slightly inaccurate comment on loadLicenseString return
The comment says || undefined "converts an empty/whitespace-only env var", but by the time || undefined is evaluated, .trim() has already removed whitespace — so it only converts an already-empty string. The actual whitespace handling is done by .trim(). Minor phrasing issue but can mislead readers of the code.

5. Mixed RSpec patterns
The new tests in engine_spec.rb use the spy pattern (allow + have_received) while adjacent existing tests use the message expectation pattern (expect...to receive). Not a bug, but inconsistent style within the same spec file. Prefer expect...to receive for consistency with surrounding tests.

Positive Notes

  • The UNINITIALIZED Symbol sentinel fix for cachedLicenseOrganization / cachedLicensePlan is correct and well-motivated.
  • load_license_string in Ruby is now beautifully concise using .presence.
  • The TODO comment includes a concrete target date and version, which is good practice.
  • Whitespace/newline trimming tests are a nice addition.
  • Documentation changes are clear and complete.

// The caching here is deterministic - given the same environment variable value, every
// worker will compute the same cached values. Redundant computation across workers
// is acceptable since license validation is infrequent (once per worker startup).
let cachedLicenseStatus: LicenseStatus | undefined;
Copy link

Choose a reason for hiding this comment

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

The cachedLicenseStatus variable uses undefined as its "not yet initialized" sentinel, while cachedLicenseOrganization and cachedLicensePlan use the UNINITIALIZED symbol specifically to distinguish between "not yet computed" and "computed as undefined".

This inconsistency works today only because LicenseStatus never includes undefined in its union. But if that assumption ever changes, this check silently breaks. For consistency and future safety, consider:

Suggested change
let cachedLicenseStatus: LicenseStatus | undefined;
const UNINITIALIZED = Symbol('uninitialized');
let cachedLicenseStatus: LicenseStatus | typeof UNINITIALIZED = UNINITIALIZED;
let cachedLicenseOrganization: string | undefined | typeof UNINITIALIZED = UNINITIALIZED;
let cachedLicensePlan: ValidPlan | undefined | typeof UNINITIALIZED = UNINITIALIZED;

And update getLicenseStatus() to check cachedLicenseStatus !== UNINITIALIZED, and reset() to set it back to UNINITIALIZED.

return undefined;
// `|| undefined` converts an empty/whitespace-only env var to undefined,
// so it is reported as 'missing' rather than 'invalid'.
return envLicense || undefined;
Copy link

Choose a reason for hiding this comment

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

The comment is slightly inaccurate: .trim() already handles the whitespace removal. By the time || undefined executes, envLicense is either undefined (env var not set) or a trimmed string. The || undefined only converts the empty-string case. Consider:

Suggested change
return envLicense || undefined;
// `.trim()` strips surrounding whitespace/newlines; `|| undefined` converts the resulting
// empty string to `undefined` so it is reported as 'missing' rather than 'invalid'.
return envLicense || undefined;

log_legacy_license_migration_notice if legacy_license_file_present?
log_license_issue("No license found", "Get a license at #{LICENSE_URL}")
when :expired
expiration = ReactOnRailsPro::LicenseValidator.license_expiration
Copy link

Choose a reason for hiding this comment

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

The legacy file migration notice is only emitted for :missing and :valid statuses. If a user has the legacy file present AND has set REACT_ON_RAILS_PRO_LICENSE to an expired or invalid token, they get no hint that the legacy file is irrelevant.

While less common, this could confuse users who think the key file might be the fallback. Consider adding the cleanup/migration notice to the :expired and :invalid branches as well, or at minimum add a comment here explaining why those cases are intentionally excluded.

it "logs migration warning for env-var setup" do
allow(mock_logger).to receive(:warn)
described_class.log_license_status
expect(mock_logger).to have_received(:warn).with(/legacy license file/)
Copy link

Choose a reason for hiding this comment

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

The three have_received(:warn) assertions here are misleading — the first two (/legacy license file/ and /REACT_ON_RAILS_PRO_LICENSE/) both match the same single warn call (the migration notice), since the migration message contains both substrings. This makes the test look like it's verifying three distinct log statements when it's really only verifying two.

Consider either combining into one matcher or asserting the exact number of warn calls:

expect(mock_logger).to have_received(:warn).with(/Detected legacy license file.*REACT_ON_RAILS_PRO_LICENSE/m)
expect(mock_logger).to have_received(:warn).with(/No license found/)
expect(mock_logger).to have_received(:warn).exactly(2).times

@ihabadham
Copy link
Collaborator Author

All five items reviewed — no changes needed:

  1. Expired/invalid + legacy file: If the env var is set (even to a bad value), the user has already migrated. The migration notice doesn't apply, and the cleanup notice is secondary to the actual problem (expired/invalid token).

  2. Overlapping matchers: Both matchers happen to match the same warn call, but have_received checks all calls independently — the test is correct. Third assertion (/No license found/) verifies the separate log_license_issue call.

  3. Cache sentinel asymmetry: Addressed in two previous rounds. Intentional — LicenseStatus is a string union that can never be undefined.

  4. Comment accuracy: The comment describes the end-to-end effect (whitespace-only env var → undefined). The fact that .trim() handles the whitespace step is an implementation detail — the comment explains why || undefined exists.

  5. Mixed RSpec patterns: The spy pattern (allow + have_received) was already used by the adjacent legacy-file tests before this commit. Matched surrounding style.

@ihabadham ihabadham merged commit 1123f0c into master Feb 24, 2026
47 checks passed
@ihabadham ihabadham deleted the ihabadham/chore/remove-license-key-file-option-ror branch February 24, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review No final review yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant