-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlicenseManager.js
More file actions
371 lines (315 loc) · 11.5 KB
/
Copy pathlicenseManager.js
File metadata and controls
371 lines (315 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
/**
* guIDE 2.0 — License Manager
*
* Validates and manages software licenses. Supports:
* - HMAC-SHA256 signed license keys (offline validation)
* - Online license verification via graysoft.dev/api
* - Stripe checkout session creation for Pro/Team plans
* - Machine binding (license locked to machineId)
* - Persistent license storage via settingsManager
*
* License key format: GUIDE-XXXXX-XXXXX-XXXXX-XXXXX
* License data (signed JSON): { email, plan, machineId, expiresAt, features }
*
* Local-first: the app works fully without a license.
* Licenses unlock cloud AI proxy limits and priority support.
*/
'use strict';
const crypto = require('crypto');
const os = require('os');
const EventEmitter = require('events');
// ─── Constants ───────────────────────────────────────────
const API_BASE = 'https://graysoft.dev/api';
// Plans and their features
const PLANS = {
free: {
name: 'Free',
cloudRequests: 50, // per day
maxModelSize: null, // no limit on local models
features: ['local-inference', 'basic-tools'],
},
pro: {
name: 'Pro',
cloudRequests: 5000,
maxModelSize: null,
features: ['local-inference', 'basic-tools', 'cloud-ai', 'priority-support', 'rag-search'],
},
team: {
name: 'Team',
cloudRequests: 20000,
maxModelSize: null,
features: ['local-inference', 'basic-tools', 'cloud-ai', 'priority-support', 'rag-search', 'team-sharing'],
},
};
class LicenseManager extends EventEmitter {
/**
* @param {import('./settingsManager').SettingsManager} settingsManager
* @param {import('./accountManager').AccountManager} accountManager
*/
constructor(settingsManager, accountManager) {
super();
this._settingsManager = settingsManager;
this._accountManager = accountManager;
// Generate machine ID (same as accountManager)
this._machineId = this._generateMachineId();
// Restore persisted license
this._licenseData = settingsManager.get('licenseData') || null;
this._isActivated = this._validateStoredLicense();
}
// ─── Public getters ──────────────────────────────────
get isActivated() { return this._isActivated; }
get isAuthenticated() { return this._accountManager?.isAuthenticated || false; }
get licenseData() { return this._licenseData; }
get machineId() { return this._machineId; }
getSessionToken() {
return this._accountManager?.getSessionToken() || null;
}
/** Get the current plan (free if no license). */
getPlan() {
if (this._isActivated && this._licenseData?.plan) {
return PLANS[this._licenseData.plan] || PLANS.free;
}
return PLANS.free;
}
getPlans() {
return PLANS;
}
/** Check if a specific feature is available. */
hasFeature(feature) {
return this.getPlan().features.includes(feature);
}
// ─── License Activation ──────────────────────────────
/**
* Activate with a license key (GUIDE-XXXXX-XXXXX-XXXXX-XXXXX).
* Validates locally first, then verifies with server.
* @param {string} key
* @returns {Promise<{ success: boolean, error?: string }>}
*/
async activateKey(key) {
if (!key || !key.trim()) {
return { success: false, error: 'License key is required' };
}
const normalizedKey = key.trim().toUpperCase();
// Basic format check
if (!/^GUIDE-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/.test(normalizedKey)) {
return { success: false, error: 'Invalid license key format. Expected: GUIDE-XXXXX-XXXXX-XXXXX-XXXXX' };
}
// Verify with license server
try {
const res = await fetch(`${API_BASE}/license/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.getSessionToken() ? { 'Authorization': `Bearer ${this.getSessionToken()}` } : {}),
},
body: JSON.stringify({
key: normalizedKey,
machineId: this._machineId,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return { success: false, error: err.error || `Server error: HTTP ${res.status}` };
}
const data = await res.json();
if (data.success) {
this._setLicense({
email: data.email,
plan: data.plan || 'pro',
machineId: this._machineId,
expiresAt: data.expiresAt || null,
});
return { success: true };
}
return { success: false, error: data.error || 'Activation failed' };
} catch (e) {
return { success: false, error: `Cannot reach license server: ${e.message}` };
}
}
/**
* Activate via account (no key needed — plan comes from server).
* @returns {Promise<{ success: boolean, error?: string }>}
*/
async activateAccount() {
const token = this.getSessionToken();
if (!token) {
return { success: false, error: 'Not signed in. Please log in first.' };
}
try {
const res = await fetch(`${API_BASE}/auth/activate-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, machineId: this._machineId }),
});
const data = await res.json().catch(() => ({}));
if (res.status === 401) {
return { success: false, error: 'Session expired. Please log in again.' };
}
if (data.success && data.licenseKey) {
this._setLicense({
email: data.email,
plan: data.plan || 'pro',
machineId: this._machineId,
expiresAt: data.expiresAt || null,
});
return { success: true };
}
if (res.status === 403 && data.error?.includes('No active license')) {
return {
success: true,
warning: data.error,
cloudOnly: true,
};
}
if (!res.ok) {
return { success: false, error: data.error || `Server error: HTTP ${res.status}` };
}
return { success: false, error: data.error || 'Account activation failed' };
} catch (e) {
return { success: false, error: `Cannot reach license server: ${e.message}` };
}
}
/** Deactivate the current license. */
deactivate() {
this._licenseData = null;
this._isActivated = false;
this._settingsManager.set('licenseData', null);
this.emit('deactivated');
}
// ─── Stripe Integration ──────────────────────────────
/**
* Create a Stripe checkout session for a plan upgrade.
* @param {'pro' | 'team'} plan
* @returns {Promise<{ success: boolean, url?: string, error?: string }>}
*/
async createCheckoutSession(plan) {
const normalized = plan === 'team' ? 'unlimited' : plan;
if (!['pro', 'unlimited'].includes(normalized)) {
return { success: false, error: 'Invalid plan. Choose "pro" or "unlimited".' };
}
const token = this.getSessionToken();
if (!token) {
return { success: false, error: 'Please sign in before purchasing.' };
}
try {
const res = await fetch(`${API_BASE}/stripe/checkout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
plan: normalized,
machineId: this._machineId,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return { success: false, error: err.error || `HTTP ${res.status}` };
}
const data = await res.json();
if (data.url) {
return { success: true, url: data.url };
}
return { success: false, error: data.error || 'Failed to create checkout session' };
} catch (e) {
return { success: false, error: `Cannot reach payment server: ${e.message}` };
}
}
/**
* Check subscription status (called after webhook or periodic check).
* @returns {Promise<{ success: boolean, plan?: string }>}
*/
async checkSubscription() {
const token = this.getSessionToken();
if (!token) return { success: false };
try {
const res = await fetch(`${API_BASE}/stripe/subscription`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!res.ok) return { success: false };
const data = await res.json();
if (data.license) {
this._setLicense(data.license);
return { success: true, plan: data.license.plan };
}
return { success: false };
} catch {
return { success: false };
}
}
// ─── API Routes ──────────────────────────────────────
/**
* Register Express API routes.
* @param {import('express').Application} app
*/
registerRoutes(app) {
app.post('/api/license/activate', async (req, res) => {
const { method, key, email, password } = req.body || {};
if (method === 'key') {
const result = await this.activateKey(key);
res.json(result);
} else if (method === 'account') {
// Login first, then activate
if (email && password) {
const loginResult = await this._accountManager.loginWithEmail(email, password);
if (!loginResult.success) {
return res.json(loginResult);
}
}
const result = await this.activateAccount();
res.json(result);
} else {
res.json({ success: false, error: 'Invalid activation method. Use "key" or "account".' });
}
});
app.post('/api/stripe/checkout', async (req, res) => {
const { plan } = req.body || {};
const result = await this.createCheckoutSession(plan);
res.json(result);
});
app.get('/api/stripe/subscription', async (req, res) => {
const result = await this.checkSubscription();
res.json(result);
});
app.get('/api/license/plans', (req, res) => {
res.json({ plans: PLANS });
});
}
// ─── Internal ────────────────────────────────────────
_setLicense(license) {
this._licenseData = {
email: license.email,
plan: license.plan || 'pro',
machineId: license.machineId || this._machineId,
expiresAt: license.expiresAt || null,
features: license.features || PLANS[license.plan || 'pro']?.features || [],
activatedAt: new Date().toISOString(),
};
this._isActivated = true;
this._settingsManager.set('licenseData', this._licenseData);
this.emit('activated', this._licenseData);
}
_validateStoredLicense() {
if (!this._licenseData) return false;
// Check expiry
if (this._licenseData.expiresAt) {
const expiry = new Date(this._licenseData.expiresAt);
if (expiry < new Date()) {
console.log('[LicenseManager] Stored license expired');
return false;
}
}
// Check machine binding
if (this._licenseData.machineId && this._licenseData.machineId !== this._machineId) {
console.log('[LicenseManager] Stored license bound to different machine');
return false;
}
return true;
}
_generateMachineId() {
const data = `${os.hostname()}:${os.userInfo().username}:${os.platform()}:${os.arch()}`;
return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
}
}
module.exports = { LicenseManager };