@@ -10,7 +10,7 @@ interface LicenseData {
1010 plan ?: string ; // Optional: license plan (e.g., "free", "paid")
1111 issued_by ?: string ; // Optional: who issued the license
1212 // Allow additional fields
13- [ key : string ] : any ;
13+ [ key : string ] : unknown ;
1414}
1515
1616// Module-level state for caching
@@ -19,54 +19,88 @@ let cachedLicenseData: LicenseData | undefined;
1919let cachedValidationError : string | undefined ;
2020
2121/**
22- * Validates the license and exits the process if invalid.
23- * Caches the result after first validation.
24- *
25- * @throws Exits process if license is invalid
22+ * Handles invalid license by logging error and exiting.
23+ * @private
2624 */
27- export function validateLicense ( ) : void {
28- if ( cachedValid !== undefined ) {
29- return ;
30- }
31-
32- cachedValid = performValidation ( ) ;
25+ function handleInvalidLicense ( message : string ) : never {
26+ const fullMessage = `[React on Rails Pro] ${ message } ` ;
27+ console . error ( fullMessage ) ;
28+ // Validation errors should prevent the application from starting
29+ process . exit ( 1 ) ;
3330}
3431
3532/**
36- * Gets the decoded license data.
37- *
38- * @returns Decoded license data or undefined if no license
33+ * Logs license information for analytics.
34+ * @private
3935 */
40- export function getLicenseData ( ) : LicenseData | undefined {
41- if ( ! cachedLicenseData ) {
42- loadAndDecodeLicense ( ) ;
36+ function logLicenseInfo ( license : LicenseData ) : void {
37+ const { plan, issued_by : issuedBy } = license ;
38+
39+ if ( plan ) {
40+ console . log ( `[React on Rails Pro] License plan: ${ plan } ` ) ;
41+ }
42+ if ( issuedBy ) {
43+ console . log ( `[React on Rails Pro] Issued by: ${ issuedBy } ` ) ;
4344 }
44- return cachedLicenseData ;
4545}
4646
4747/**
48- * Gets the validation error message if validation failed.
49- *
50- * @returns Error message or undefined
48+ * Loads the license string from environment variable or config file.
49+ * @private
5150 */
52- export function getValidationError ( ) : string | undefined {
53- return cachedValidationError ;
51+ // eslint-disable-next-line consistent-return
52+ function loadLicenseString ( ) : string | never {
53+ // First try environment variable
54+ const envLicense = process . env . REACT_ON_RAILS_PRO_LICENSE ;
55+ if ( envLicense ) {
56+ return envLicense ;
57+ }
58+
59+ // Then try config file (relative to project root)
60+ try {
61+ const configPath = path . join ( process . cwd ( ) , 'config' , 'react_on_rails_pro_license.key' ) ;
62+ if ( fs . existsSync ( configPath ) ) {
63+ return fs . readFileSync ( configPath , 'utf8' ) . trim ( ) ;
64+ }
65+ } catch {
66+ // File doesn't exist or can't be read
67+ }
68+
69+ cachedValidationError =
70+ 'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
71+ 'or create config/react_on_rails_pro_license.key file. ' +
72+ 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
73+
74+ handleInvalidLicense ( cachedValidationError ) ;
5475}
5576
5677/**
57- * Resets all cached validation state (primarily for testing).
78+ * Loads and decodes the license from environment or file.
79+ * @private
5880 */
59- export function reset ( ) : void {
60- cachedValid = undefined ;
61- cachedLicenseData = undefined ;
62- cachedValidationError = undefined ;
81+ function loadAndDecodeLicense ( ) : LicenseData {
82+ const licenseString = loadLicenseString ( ) ;
83+
84+ const decoded = jwt . verify ( licenseString , PUBLIC_KEY , {
85+ // Enforce RS256 algorithm only to prevent "alg=none" and downgrade attacks.
86+ // Adding other algorithms to the whitelist (e.g., ['RS256', 'HS256']) can introduce vulnerabilities:
87+ // If the public key is mistakenly used as a secret for HMAC algorithms (like HS256), attackers could forge tokens.
88+ // Always carefully review algorithm changes to avoid signature bypass risks.
89+ algorithms : [ 'RS256' ] ,
90+ // Disable automatic expiration verification so we can handle it manually with custom logic
91+ ignoreExpiration : true ,
92+ } ) as LicenseData ;
93+
94+ cachedLicenseData = decoded ;
95+ return decoded ;
6396}
6497
6598/**
6699 * Performs the actual license validation logic.
67100 * @private
68101 */
69- function performValidation ( ) : boolean {
102+ // eslint-disable-next-line consistent-return
103+ function performValidation ( ) : boolean | never {
70104 try {
71105 const license = loadAndDecodeLicense ( ) ;
72106
@@ -92,93 +126,67 @@ function performValidation(): boolean {
92126 logLicenseInfo ( license ) ;
93127
94128 return true ;
95- } catch ( error : any ) {
96- if ( error . name === 'JsonWebTokenError' ) {
129+ } catch ( error : unknown ) {
130+ if ( error instanceof Error && error . name === 'JsonWebTokenError' ) {
97131 cachedValidationError =
98132 `Invalid license signature: ${ error . message } . ` +
99133 'Your license file may be corrupted. ' +
100134 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
101- } else {
135+ } else if ( error instanceof Error ) {
102136 cachedValidationError =
103137 `License validation error: ${ error . message } . ` +
104138 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
139+ } else {
140+ cachedValidationError =
141+ 'License validation error: Unknown error. ' +
142+ 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
105143 }
106144 handleInvalidLicense ( cachedValidationError ) ;
107145 }
108146}
109147
110148/**
111- * Loads and decodes the license from environment or file.
112- * @private
149+ * Validates the license and exits the process if invalid.
150+ * Caches the result after first validation.
151+ *
152+ * @returns true if license is valid
153+ * @throws Exits process if license is invalid
113154 */
114- function loadAndDecodeLicense ( ) : LicenseData {
115- const licenseString = loadLicenseString ( ) ;
116-
117- const decoded = jwt . verify ( licenseString , PUBLIC_KEY , {
118- // Enforce RS256 algorithm only to prevent "alg=none" and downgrade attacks.
119- // Adding other algorithms to the whitelist (e.g., ['RS256', 'HS256']) can introduce vulnerabilities:
120- // If the public key is mistakenly used as a secret for HMAC algorithms (like HS256), attackers could forge tokens.
121- // Always carefully review algorithm changes to avoid signature bypass risks.
122- algorithms : [ 'RS256' ] ,
123- // Disable automatic expiration verification so we can handle it manually with custom logic
124- ignoreExpiration : true ,
125- } ) as LicenseData ;
155+ export function validateLicense ( ) : boolean {
156+ if ( cachedValid !== undefined ) {
157+ return cachedValid ;
158+ }
126159
127- cachedLicenseData = decoded ;
128- return decoded ;
160+ cachedValid = performValidation ( ) ;
161+ return cachedValid ;
129162}
130163
131164/**
132- * Loads the license string from environment variable or config file.
133- * @private
165+ * Gets the decoded license data.
166+ *
167+ * @returns Decoded license data or undefined if no license
134168 */
135- function loadLicenseString ( ) : string {
136- // First try environment variable
137- const envLicense = process . env . REACT_ON_RAILS_PRO_LICENSE ;
138- if ( envLicense ) {
139- return envLicense ;
140- }
141-
142- // Then try config file (relative to project root)
143- try {
144- const configPath = path . join ( process . cwd ( ) , 'config' , 'react_on_rails_pro_license.key' ) ;
145- if ( fs . existsSync ( configPath ) ) {
146- return fs . readFileSync ( configPath , 'utf8' ) . trim ( ) ;
147- }
148- } catch ( error ) {
149- // File doesn't exist or can't be read
169+ export function getLicenseData ( ) : LicenseData | undefined {
170+ if ( ! cachedLicenseData ) {
171+ loadAndDecodeLicense ( ) ;
150172 }
151-
152- cachedValidationError =
153- 'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
154- 'or create config/react_on_rails_pro_license.key file. ' +
155- 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
156-
157- handleInvalidLicense ( cachedValidationError ) ;
173+ return cachedLicenseData ;
158174}
159175
160176/**
161- * Handles invalid license by logging error and exiting.
162- * @private
177+ * Gets the validation error message if validation failed.
178+ *
179+ * @returns Error message or undefined
163180 */
164- function handleInvalidLicense ( message : string ) : never {
165- const fullMessage = `[React on Rails Pro] ${ message } ` ;
166- console . error ( fullMessage ) ;
167- // Validation errors should prevent the application from starting
168- process . exit ( 1 ) ;
181+ export function getValidationError ( ) : string | undefined {
182+ return cachedValidationError ;
169183}
170184
171185/**
172- * Logs license information for analytics.
173- * @private
186+ * Resets all cached validation state (primarily for testing).
174187 */
175- function logLicenseInfo ( license : LicenseData ) : void {
176- const { plan, issued_by : issuedBy } = license ;
177-
178- if ( plan ) {
179- console . log ( `[React on Rails Pro] License plan: ${ plan } ` ) ;
180- }
181- if ( issuedBy ) {
182- console . log ( `[React on Rails Pro] Issued by: ${ issuedBy } ` ) ;
183- }
188+ export function reset ( ) : void {
189+ cachedValid = undefined ;
190+ cachedLicenseData = undefined ;
191+ cachedValidationError = undefined ;
184192}
0 commit comments