@@ -10,167 +10,171 @@ interface LicenseData {
1010 [ key : string ] : any ;
1111}
1212
13- class LicenseValidator {
14- private static instance : LicenseValidator ;
15- private valid ?: boolean ;
16- private licenseData ?: LicenseData ;
17- private validationError ?: string ;
18-
19- private constructor ( ) { }
20-
21- public static getInstance ( ) : LicenseValidator {
22- if ( ! LicenseValidator . instance ) {
23- LicenseValidator . instance = new LicenseValidator ( ) ;
24- }
25- return LicenseValidator . instance ;
13+ // Module-level state for caching
14+ let cachedValid : boolean | undefined ;
15+ let cachedLicenseData : LicenseData | undefined ;
16+ let cachedValidationError : string | undefined ;
17+
18+ /**
19+ * Validates the license and raises an error if invalid.
20+ * Caches the result after first validation.
21+ *
22+ * @returns true if license is valid
23+ * @throws Exits process if license is invalid
24+ */
25+ export function validateLicense ( ) : boolean {
26+ if ( cachedValid !== undefined ) {
27+ return cachedValid ;
2628 }
2729
28- public isValid ( ) : boolean {
29- if ( this . valid !== undefined ) {
30- return this . valid ;
31- }
32-
33- this . valid = this . validateLicense ( ) ;
34- return this . valid ;
35- }
30+ cachedValid = performValidation ( ) ;
31+ return cachedValid ;
32+ }
3633
37- public getLicenseData ( ) : LicenseData | undefined {
38- if ( ! this . licenseData ) {
39- this . loadAndDecodeLicense ( ) ;
40- }
41- return this . licenseData ;
34+ /**
35+ * Gets the decoded license data.
36+ *
37+ * @returns Decoded license data or undefined if no license
38+ */
39+ export function getLicenseData ( ) : LicenseData | undefined {
40+ if ( ! cachedLicenseData ) {
41+ loadAndDecodeLicense ( ) ;
4242 }
43+ return cachedLicenseData ;
44+ }
4345
44- public getValidationError ( ) : string | undefined {
45- return this . validationError ;
46- }
46+ /**
47+ * Gets the validation error message if validation failed.
48+ *
49+ * @returns Error message or undefined
50+ */
51+ export function getValidationError ( ) : string | undefined {
52+ return cachedValidationError ;
53+ }
4754
48- public reset ( ) : void {
49- this . valid = undefined ;
50- this . licenseData = undefined ;
51- this . validationError = undefined ;
52- }
55+ /**
56+ * Resets all cached validation state (primarily for testing).
57+ */
58+ export function reset ( ) : void {
59+ cachedValid = undefined ;
60+ cachedLicenseData = undefined ;
61+ cachedValidationError = undefined ;
62+ }
5363
54- private validateLicense ( ) : boolean {
55- try {
56- const license = this . loadAndDecodeLicense ( ) ;
57- if ( ! license ) {
58- return false ;
59- }
60-
61- // Check that exp field exists
62- if ( ! license . exp ) {
63- this . validationError = 'License is missing required expiration field. ' +
64- 'Your license may be from an older version. ' +
65- 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
66- this . handleInvalidLicense ( this . validationError ) ;
67- return false ;
68- }
69-
70- // Check expiry
71- if ( Date . now ( ) / 1000 > license . exp ) {
72- this . validationError = 'License has expired. ' +
73- 'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
74- 'or upgrade to a paid license for production use.' ;
75- this . handleInvalidLicense ( this . validationError ) ;
76- return false ;
77- }
78-
79- // Log license type if present (for analytics)
80- this . logLicenseInfo ( license ) ;
81-
82- return true ;
83- } catch ( error : any ) {
84- if ( error . name === 'JsonWebTokenError' ) {
85- this . validationError = `Invalid license signature: ${ error . message } . ` +
86- 'Your license file may be corrupted. ' +
87- 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
88- } else {
89- this . validationError = `License validation error: ${ error . message } . ` +
90- 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
91- }
92- this . handleInvalidLicense ( this . validationError ) ;
93- return false ;
64+ /**
65+ * Performs the actual license validation logic.
66+ * @private
67+ */
68+ function performValidation ( ) : boolean {
69+ try {
70+ const license = loadAndDecodeLicense ( ) ;
71+
72+ // Check that exp field exists
73+ if ( ! license . exp ) {
74+ cachedValidationError =
75+ 'License is missing required expiration field. ' +
76+ 'Your license may be from an older version. ' +
77+ 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
78+ handleInvalidLicense ( cachedValidationError ) ;
9479 }
95- }
9680
97- private loadAndDecodeLicense ( ) : LicenseData | undefined {
98- const licenseString = this . loadLicenseString ( ) ;
99- if ( ! licenseString ) {
100- return undefined ;
81+ // Check expiry
82+ if ( Date . now ( ) / 1000 > license . exp ) {
83+ cachedValidationError =
84+ 'License has expired. ' +
85+ 'Get a FREE evaluation license (3 months) at https://shakacode.com/react-on-rails-pro ' +
86+ 'or upgrade to a paid license for production use.' ;
87+ handleInvalidLicense ( cachedValidationError ) ;
10188 }
10289
103- try {
104- const decoded = jwt . verify ( licenseString , PUBLIC_KEY , {
105- algorithms : [ 'RS256' ] ,
106- // Disable automatic expiration verification so we can handle it manually with custom logic
107- ignoreExpiration : true
108- } ) as LicenseData ;
109-
110- this . licenseData = decoded ;
111- return decoded ;
112- } catch ( error ) {
113- throw error ;
90+ // Log license type if present (for analytics)
91+ logLicenseInfo ( license ) ;
92+
93+ return true ;
94+ } catch ( error : any ) {
95+ if ( error . name === 'JsonWebTokenError' ) {
96+ cachedValidationError =
97+ `Invalid license signature: ${ error . message } . ` +
98+ 'Your license file may be corrupted. ' +
99+ 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
100+ } else {
101+ cachedValidationError =
102+ `License validation error: ${ error . message } . ` +
103+ 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
114104 }
105+ handleInvalidLicense ( cachedValidationError ) ;
115106 }
107+ }
116108
117- private loadLicenseString ( ) : string | undefined {
118- // First try environment variable
119- const envLicense = process . env . REACT_ON_RAILS_PRO_LICENSE ;
120- if ( envLicense ) {
121- return envLicense ;
122- }
123-
124- // Then try config file (relative to project root)
125- try {
126- const configPath = path . join ( process . cwd ( ) , 'config' , 'react_on_rails_pro_license.key' ) ;
127- if ( fs . existsSync ( configPath ) ) {
128- return fs . readFileSync ( configPath , 'utf8' ) . trim ( ) ;
129- }
130- } catch ( error ) {
131- // File doesn't exist or can't be read
132- }
133-
134- this . validationError =
135- 'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
136- 'or create config/react_on_rails_pro_license.key file. ' +
137- 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
138-
139- this . handleInvalidLicense ( this . validationError ) ;
140-
141- return undefined ;
142- }
109+ /**
110+ * Loads and decodes the license from environment or file.
111+ * @private
112+ */
113+ function loadAndDecodeLicense ( ) : LicenseData {
114+ const licenseString = loadLicenseString ( ) ;
115+
116+ const decoded = jwt . verify ( licenseString , PUBLIC_KEY , {
117+ algorithms : [ 'RS256' ] ,
118+ // Disable automatic expiration verification so we can handle it manually with custom logic
119+ ignoreExpiration : true ,
120+ } ) as LicenseData ;
121+
122+ cachedLicenseData = decoded ;
123+ return decoded ;
124+ }
143125
144- private handleInvalidLicense ( message : string ) : void {
145- const fullMessage = `[React on Rails Pro] ${ message } ` ;
146- console . error ( fullMessage ) ;
147- // Validation errors should prevent the application from starting
148- process . exit ( 1 ) ;
126+ /**
127+ * Loads the license string from environment variable or config file.
128+ * @private
129+ */
130+ function loadLicenseString ( ) : string {
131+ // First try environment variable
132+ const envLicense = process . env . REACT_ON_RAILS_PRO_LICENSE ;
133+ if ( envLicense ) {
134+ return envLicense ;
149135 }
150136
151- private logLicenseInfo ( license : LicenseData ) : void {
152- const plan = ( license as any ) . plan ;
153- const issuedBy = ( license as any ) . issued_by ;
154-
155- if ( plan ) {
156- console . log ( `[React on Rails Pro] License plan: ${ plan } ` ) ;
157- }
158- if ( issuedBy ) {
159- console . log ( `[React on Rails Pro] Issued by: ${ issuedBy } ` ) ;
137+ // Then try config file (relative to project root)
138+ try {
139+ const configPath = path . join ( process . cwd ( ) , 'config' , 'react_on_rails_pro_license.key' ) ;
140+ if ( fs . existsSync ( configPath ) ) {
141+ return fs . readFileSync ( configPath , 'utf8' ) . trim ( ) ;
160142 }
143+ } catch ( error ) {
144+ // File doesn't exist or can't be read
161145 }
162- }
163146
164- export const licenseValidator = LicenseValidator . getInstance ( ) ;
147+ cachedValidationError =
148+ 'No license found. Please set REACT_ON_RAILS_PRO_LICENSE environment variable ' +
149+ 'or create config/react_on_rails_pro_license.key file. ' +
150+ 'Get a FREE evaluation license at https://shakacode.com/react-on-rails-pro' ;
165151
166- export function isLicenseValid ( ) : boolean {
167- return licenseValidator . isValid ( ) ;
152+ handleInvalidLicense ( cachedValidationError ) ;
168153}
169154
170- export function getLicenseData ( ) : LicenseData | undefined {
171- return licenseValidator . getLicenseData ( ) ;
155+ /**
156+ * Handles invalid license by logging error and exiting.
157+ * @private
158+ */
159+ function handleInvalidLicense ( message : string ) : never {
160+ const fullMessage = `[React on Rails Pro] ${ message } ` ;
161+ console . error ( fullMessage ) ;
162+ // Validation errors should prevent the application from starting
163+ process . exit ( 1 ) ;
172164}
173165
174- export function getLicenseValidationError ( ) : string | undefined {
175- return licenseValidator . getValidationError ( ) ;
166+ /**
167+ * Logs license information for analytics.
168+ * @private
169+ */
170+ function logLicenseInfo ( license : LicenseData ) : void {
171+ const plan = ( license as any ) . plan ;
172+ const issuedBy = ( license as any ) . issued_by ;
173+
174+ if ( plan ) {
175+ console . log ( `[React on Rails Pro] License plan: ${ plan } ` ) ;
176+ }
177+ if ( issuedBy ) {
178+ console . log ( `[React on Rails Pro] Issued by: ${ issuedBy } ` ) ;
179+ }
176180}
0 commit comments