filemayor 3.6.0 → 4.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +90 -90
- package/README.md +96 -96
- package/core/analyzer.js +163 -163
- package/core/categories.js +235 -235
- package/core/cleaner.js +527 -527
- package/core/config.js +562 -562
- package/core/fs-abstraction.js +110 -38
- package/core/index.js +135 -135
- package/core/intent-interpreter.js +228 -66
- package/core/license.js +403 -339
- package/core/organizer.js +414 -414
- package/core/reporter.js +783 -783
- package/core/scanner.js +466 -466
- package/core/security.js +370 -370
- package/core/sop-parser.js +564 -564
- package/core/telemetry.js +74 -0
- package/core/vault.js +83 -69
- package/core/watcher.js +478 -478
- package/index.js +895 -877
- package/package.json +3 -3
package/core/license.js
CHANGED
|
@@ -1,339 +1,403 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
-
* FILEMAYOR CORE — LICENSE
|
|
6
|
-
* Offline license key validation, tier management, and feature gating.
|
|
7
|
-
* Created by Lehlohonolo Goodwill Nchefu (Chevza)
|
|
8
|
-
* ═══════════════════════════════════════════════════════════════════
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const os = require('os');
|
|
16
|
-
const crypto = require('crypto');
|
|
17
|
-
|
|
18
|
-
// ─── Constants ────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const LICENSE_FILE = path.join(os.homedir(), '.filemayor-license.json');
|
|
21
|
-
const KEY_PREFIX = 'FM';
|
|
22
|
-
const CHECKSUM_SECRET = process.env.FM_CHECKSUM_SECRET || 'filemayor-chevza-2026';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* License tiers with capabilities
|
|
26
|
-
*/
|
|
27
|
-
const TIERS = {
|
|
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
|
-
key:
|
|
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
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* FILEMAYOR CORE — LICENSE
|
|
6
|
+
* Offline license key validation, tier management, and feature gating.
|
|
7
|
+
* Created by Lehlohonolo Goodwill Nchefu (Chevza)
|
|
8
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
// ─── Constants ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const LICENSE_FILE = path.join(os.homedir(), '.filemayor-license.json');
|
|
21
|
+
const KEY_PREFIX = 'FM';
|
|
22
|
+
const CHECKSUM_SECRET = process.env.FM_CHECKSUM_SECRET || 'filemayor-chevza-2026';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* License tiers with capabilities
|
|
26
|
+
*/
|
|
27
|
+
const TIERS = {
|
|
28
|
+
// Free until 100k users: every feature is open. The tier system and
|
|
29
|
+
// Ed25519 license verification stay in place so paid tiers can be
|
|
30
|
+
// reintroduced later without re-architecting.
|
|
31
|
+
free: {
|
|
32
|
+
name: 'Free',
|
|
33
|
+
features: ['scan', 'organize', 'clean', 'undo', 'init', 'info',
|
|
34
|
+
'watch', 'sop-ai', 'bulk-organize', 'csv-export'],
|
|
35
|
+
limits: { bulkOrganize: Infinity },
|
|
36
|
+
},
|
|
37
|
+
pro: {
|
|
38
|
+
name: 'Pro',
|
|
39
|
+
features: ['scan', 'organize', 'clean', 'undo', 'init', 'info',
|
|
40
|
+
'watch', 'sop-ai', 'bulk-organize', 'csv-export'],
|
|
41
|
+
limits: { bulkOrganize: Infinity },
|
|
42
|
+
},
|
|
43
|
+
enterprise: {
|
|
44
|
+
name: 'Enterprise',
|
|
45
|
+
features: ['scan', 'organize', 'clean', 'undo', 'init', 'info',
|
|
46
|
+
'watch', 'sop-ai', 'bulk-organize', 'csv-export',
|
|
47
|
+
'api-access', 'custom-categories', 'team-sharing'],
|
|
48
|
+
limits: { bulkOrganize: Infinity },
|
|
49
|
+
},
|
|
50
|
+
owner: {
|
|
51
|
+
name: 'Owner',
|
|
52
|
+
features: ['*'],
|
|
53
|
+
limits: { bulkOrganize: Infinity },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ─── Ed25519 JWT Verification ─────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decode a base64url string to a Buffer
|
|
61
|
+
*/
|
|
62
|
+
function b64urlDecode(str) {
|
|
63
|
+
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
64
|
+
const pad = padded.length % 4;
|
|
65
|
+
return Buffer.from(pad ? padded + '='.repeat(4 - pad) : padded, 'base64');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Verify an Ed25519 JWT license token.
|
|
70
|
+
* The matching public key is resolved (in priority order) from:
|
|
71
|
+
* 1. process.env.FM_LICENSE_PUBLIC_KEY_<kid>
|
|
72
|
+
* 2. process.env.FM_LICENSE_PUBLIC_KEY
|
|
73
|
+
* where <kid> comes from the JWT header's kid field.
|
|
74
|
+
*
|
|
75
|
+
* Returns the decoded payload object on success, or null on any failure.
|
|
76
|
+
* Fails secure: an unknown/missing public key always returns null.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} token - JWT string (header.payload.signature, all base64url)
|
|
79
|
+
* @returns {object|null} Decoded payload or null
|
|
80
|
+
*/
|
|
81
|
+
function verifySignedKey(token) {
|
|
82
|
+
try {
|
|
83
|
+
const parts = token.split('.');
|
|
84
|
+
if (parts.length !== 3) return null;
|
|
85
|
+
|
|
86
|
+
const [headerB64, payloadB64, sigB64] = parts;
|
|
87
|
+
const header = JSON.parse(b64urlDecode(headerB64).toString('utf8'));
|
|
88
|
+
const kid = header.kid || 'v1';
|
|
89
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
90
|
+
const signature = b64urlDecode(sigB64);
|
|
91
|
+
|
|
92
|
+
// Resolve public key PEM from environment
|
|
93
|
+
const pubKeyPem = process.env[`FM_LICENSE_PUBLIC_KEY_${kid}`]
|
|
94
|
+
|| process.env.FM_LICENSE_PUBLIC_KEY;
|
|
95
|
+
|
|
96
|
+
if (!pubKeyPem) return null; // Fail secure: no public key configured
|
|
97
|
+
|
|
98
|
+
const pubKey = crypto.createPublicKey({ key: pubKeyPem.trim(), format: 'pem' });
|
|
99
|
+
const valid = crypto.verify(null, Buffer.from(signingInput), pubKey, signature);
|
|
100
|
+
if (!valid) return null;
|
|
101
|
+
|
|
102
|
+
const payload = JSON.parse(b64urlDecode(payloadB64).toString('utf8'));
|
|
103
|
+
|
|
104
|
+
// Check expiry if present
|
|
105
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null;
|
|
106
|
+
|
|
107
|
+
return payload;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Built-in keys (hardcoded, never expire)
|
|
115
|
+
*/
|
|
116
|
+
const BUILTIN_KEYS = {
|
|
117
|
+
'FM-OWN-CHEV-2026-PRMNT': { tier: 'owner', name: 'Owner — Chevza', expires: null },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ─── Key Generation & Validation ──────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generate a checksum for a key body
|
|
124
|
+
* @param {string} body - Key body without checksum segment
|
|
125
|
+
* @returns {string} 5-char checksum
|
|
126
|
+
*/
|
|
127
|
+
function generateChecksum(body) {
|
|
128
|
+
// Ported from Admin Dashboard simpleHash for cross-platform compatibility
|
|
129
|
+
let hash = 0;
|
|
130
|
+
for (let i = 0; i < body.length; i++) {
|
|
131
|
+
hash = ((hash << 5) - hash + body.charCodeAt(i)) | 0;
|
|
132
|
+
}
|
|
133
|
+
return Math.abs(hash).toString(36).toUpperCase().padEnd(5, '0').substring(0, 5);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate key format: FM-XXX-XXXX-XXXX-XXXXX
|
|
138
|
+
* Last 5 chars are HMAC checksum of the rest
|
|
139
|
+
* @param {string} key - License key
|
|
140
|
+
* @returns {{ valid: boolean, tier: string|null, reason: string }}
|
|
141
|
+
*/
|
|
142
|
+
function validateKeyFormat(key) {
|
|
143
|
+
if (!key || typeof key !== 'string') {
|
|
144
|
+
return { valid: false, tier: null, reason: 'No key provided' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const trimmed = key.trim();
|
|
148
|
+
|
|
149
|
+
// ─── Path A: signed Ed25519 JWT (LemonSqueezy webhook output) ───
|
|
150
|
+
if (trimmed.includes('.') && trimmed.split('.').length === 3) {
|
|
151
|
+
const payload = verifySignedKey(trimmed);
|
|
152
|
+
if (payload && payload.tier && TIERS[payload.tier]) {
|
|
153
|
+
return { valid: true, tier: payload.tier, reason: 'Signed license', payload };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const k = trimmed.toUpperCase();
|
|
158
|
+
|
|
159
|
+
// ─── Path B: built-in keys (Owner, Test) — never expire ─────────
|
|
160
|
+
if (BUILTIN_KEYS[k]) {
|
|
161
|
+
return { valid: true, tier: BUILTIN_KEYS[k].tier, reason: 'Built-in key' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Path C: legacy FM-XXX-XXXX-XXXX-XXXXX checksum format ─────
|
|
165
|
+
// INTENTIONALLY DISABLED for external keys. The format's checksum
|
|
166
|
+
// is publicly derivable from source — anyone could mint Pro keys
|
|
167
|
+
// offline. We keep the format only for BUILTIN_KEYS (above) where
|
|
168
|
+
// the keys themselves are hard-coded constants. External callers
|
|
169
|
+
// sending an FM-XXX-style key get the same "invalid" response as
|
|
170
|
+
// a malformed key. Paid licenses are Ed25519 JWTs (Path A) only.
|
|
171
|
+
return { valid: false, tier: null, reason: 'Invalid license' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @deprecated FM-XXX format is no longer accepted for external (paid) licenses.
|
|
176
|
+
* The checksum is derivable from public source code. Use the LemonSqueezy
|
|
177
|
+
* webhook (filemayor-landing/app/api/lemonsqueezy/webhook/route.ts) to mint
|
|
178
|
+
* Ed25519 JWT licenses instead.
|
|
179
|
+
*
|
|
180
|
+
* This function is gated behind FM_ALLOW_LEGACY_GENERATE=true for backward
|
|
181
|
+
* compatibility with existing tooling (e.g. admin dashboard) that hasn't
|
|
182
|
+
* been migrated yet. Even when enabled, the resulting keys are NOT accepted
|
|
183
|
+
* by validateKeyFormat() unless they are also added to BUILTIN_KEYS.
|
|
184
|
+
*
|
|
185
|
+
* @param {'pro'|'enterprise'|'owner'} tier
|
|
186
|
+
* @returns {string} Generated key
|
|
187
|
+
*/
|
|
188
|
+
function generateKey(tier = 'pro') {
|
|
189
|
+
if (process.env.FM_ALLOW_LEGACY_GENERATE !== 'true') {
|
|
190
|
+
throw new Error(
|
|
191
|
+
'FM-XXX key generation is disabled. ' +
|
|
192
|
+
'Use the LemonSqueezy webhook to issue Ed25519 JWT licenses, ' +
|
|
193
|
+
'or set FM_ALLOW_LEGACY_GENERATE=true if you understand the risk.',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const tierMap = { pro: 'PRO', enterprise: 'ENT', owner: 'OWN' };
|
|
197
|
+
const code = tierMap[tier] || 'PRO';
|
|
198
|
+
const seg1 = crypto.randomBytes(2).toString('hex').toUpperCase().substring(0, 4);
|
|
199
|
+
const seg2 = crypto.randomBytes(2).toString('hex').toUpperCase().substring(0, 4);
|
|
200
|
+
const body = `FM-${code}-${seg1}-${seg2}`;
|
|
201
|
+
const checksum = generateChecksum(body);
|
|
202
|
+
return `${body}-${checksum}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── License Storage ──────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read stored license from disk
|
|
209
|
+
* @returns {{ key: string, tier: string, activatedAt: string, name: string }|null}
|
|
210
|
+
*/
|
|
211
|
+
function readLicense() {
|
|
212
|
+
try {
|
|
213
|
+
if (!fs.existsSync(LICENSE_FILE)) return null;
|
|
214
|
+
const data = JSON.parse(fs.readFileSync(LICENSE_FILE, 'utf8'));
|
|
215
|
+
return data;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Write license to disk
|
|
223
|
+
* @param {Object} licenseData
|
|
224
|
+
*/
|
|
225
|
+
function writeLicense(licenseData) {
|
|
226
|
+
fs.writeFileSync(LICENSE_FILE, JSON.stringify(licenseData, null, 2), 'utf8');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remove stored license
|
|
231
|
+
*/
|
|
232
|
+
function removeLicense() {
|
|
233
|
+
if (fs.existsSync(LICENSE_FILE)) {
|
|
234
|
+
fs.unlinkSync(LICENSE_FILE);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── License Management ──────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Activate a license key
|
|
242
|
+
* @param {string} key - License key to activate
|
|
243
|
+
* @returns {{ success: boolean, message: string, tier: string|null }}
|
|
244
|
+
*/
|
|
245
|
+
function activateLicense(key) {
|
|
246
|
+
const validation = validateKeyFormat(key);
|
|
247
|
+
|
|
248
|
+
if (!validation.valid) {
|
|
249
|
+
return { success: false, message: validation.reason, tier: null };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const k = key.trim().toUpperCase();
|
|
253
|
+
const builtin = BUILTIN_KEYS[k];
|
|
254
|
+
|
|
255
|
+
const licenseData = {
|
|
256
|
+
key: k,
|
|
257
|
+
tier: validation.tier,
|
|
258
|
+
name: builtin ? builtin.name : `${TIERS[validation.tier].name} License`,
|
|
259
|
+
activatedAt: new Date().toISOString(),
|
|
260
|
+
expires: builtin ? null : null, // LemonSqueezy keys can add expiry later
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
writeLicense(licenseData);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
message: `${TIERS[validation.tier].name} license activated successfully`,
|
|
268
|
+
tier: validation.tier,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Deactivate the current license
|
|
274
|
+
* @returns {{ success: boolean, message: string }}
|
|
275
|
+
*/
|
|
276
|
+
function deactivateLicense() {
|
|
277
|
+
const current = readLicense();
|
|
278
|
+
if (!current) {
|
|
279
|
+
return { success: false, message: 'No active license found' };
|
|
280
|
+
}
|
|
281
|
+
removeLicense();
|
|
282
|
+
return { success: true, message: 'License deactivated. Reverted to Free tier.' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get current license info
|
|
287
|
+
* @returns {{ tier: string, name: string, features: string[], limits: Object, key: string|null, active: boolean }}
|
|
288
|
+
*/
|
|
289
|
+
function getLicenseInfo() {
|
|
290
|
+
const stored = readLicense();
|
|
291
|
+
|
|
292
|
+
if (!stored) {
|
|
293
|
+
return {
|
|
294
|
+
tier: 'free',
|
|
295
|
+
name: 'Free',
|
|
296
|
+
features: TIERS.free.features,
|
|
297
|
+
limits: TIERS.free.limits,
|
|
298
|
+
key: null,
|
|
299
|
+
active: false,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Re-validate stored key
|
|
304
|
+
const validation = validateKeyFormat(stored.key);
|
|
305
|
+
if (!validation.valid) {
|
|
306
|
+
removeLicense();
|
|
307
|
+
return {
|
|
308
|
+
tier: 'free',
|
|
309
|
+
name: 'Free (invalid key removed)',
|
|
310
|
+
features: TIERS.free.features,
|
|
311
|
+
limits: TIERS.free.limits,
|
|
312
|
+
key: null,
|
|
313
|
+
active: false,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const tier = TIERS[stored.tier] || TIERS.free;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
tier: stored.tier,
|
|
321
|
+
name: stored.name,
|
|
322
|
+
features: tier.features,
|
|
323
|
+
limits: tier.limits,
|
|
324
|
+
key: stored.key,
|
|
325
|
+
active: true,
|
|
326
|
+
activatedAt: stored.activatedAt,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Feature Gating ──────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if a feature is available in the current license
|
|
334
|
+
* @param {string} feature - Feature identifier
|
|
335
|
+
* @returns {boolean}
|
|
336
|
+
*/
|
|
337
|
+
function hasFeature(feature) {
|
|
338
|
+
const info = getLicenseInfo();
|
|
339
|
+
if (info.features.includes('*')) return true; // Owner has everything
|
|
340
|
+
return info.features.includes(feature);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Check if a Pro feature is gated and return appropriate message
|
|
345
|
+
* @param {string} feature - Feature identifier
|
|
346
|
+
* @param {string} featureLabel - Human-readable feature name
|
|
347
|
+
* @returns {{ allowed: boolean, message: string|null }}
|
|
348
|
+
*/
|
|
349
|
+
function checkProFeature(feature, featureLabel) {
|
|
350
|
+
if (hasFeature(feature)) {
|
|
351
|
+
return { allowed: true, message: null };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
allowed: false,
|
|
356
|
+
message: [
|
|
357
|
+
`⚡ ${featureLabel} requires a license`,
|
|
358
|
+
'',
|
|
359
|
+
' Activate: filemayor license activate YOUR-KEY',
|
|
360
|
+
' More info: https://filemayor.com',
|
|
361
|
+
'',
|
|
362
|
+
].join('\n'),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check bulk organize limit
|
|
368
|
+
* @param {number} fileCount - Number of files to organize
|
|
369
|
+
* @returns {{ allowed: boolean, message: string|null }}
|
|
370
|
+
*/
|
|
371
|
+
function checkBulkLimit(fileCount) {
|
|
372
|
+
const info = getLicenseInfo();
|
|
373
|
+
if (info.limits.bulkOrganize >= fileCount) {
|
|
374
|
+
return { allowed: true, message: null };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
allowed: false,
|
|
379
|
+
message: [
|
|
380
|
+
`⚡ Bulk organize (${fileCount} files) exceeds your current limit`,
|
|
381
|
+
` This license tier is limited to ${info.limits.bulkOrganize} files per operation.`,
|
|
382
|
+
'',
|
|
383
|
+
' More info: https://filemayor.com',
|
|
384
|
+
].join('\n'),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Exports ─────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
TIERS,
|
|
392
|
+
BUILTIN_KEYS,
|
|
393
|
+
validateKeyFormat,
|
|
394
|
+
generateKey,
|
|
395
|
+
readLicense,
|
|
396
|
+
activateLicense,
|
|
397
|
+
deactivateLicense,
|
|
398
|
+
getLicenseInfo,
|
|
399
|
+
hasFeature,
|
|
400
|
+
checkProFeature,
|
|
401
|
+
checkBulkLimit,
|
|
402
|
+
LICENSE_FILE,
|
|
403
|
+
};
|