@mostajs/licensing-ui-react 0.1.0

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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @mostajs/licensing-ui-react
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
4
+
5
+ UI de **gestion de licences** (gate d'accès, activation, configuration) — **extraite de
6
+ `SolutionCh/MostaGare`** et **composant `@mostajs/licensing`** (DEVRULES §10 : aucune logique de
7
+ licence réécrite). **Config pilotée par `.env`.**
8
+
9
+ ```ts
10
+ import { createLicenseUiFromEnv, LicenseGate, LicenseActivationForm } from '@mostajs/licensing-ui-react';
11
+
12
+ // Contrôleur configuré depuis .env (LICENSE_SERVER_URL / LICENSE_PROJECT / LICENSE_PUBLIC_KEY)
13
+ const lic = createLicenseUiFromEnv(process.env);
14
+ lic.check(signedLicense); // vérif RSA + projet + expiration
15
+ lic.requestCode({ email }); // demande un code d'activation
16
+ lic.activate({ activationCode, machineId }); // download + verify + notify
17
+
18
+ // Composants React (Next/React) — gate d'accès
19
+ <LicenseGate project="salsabil"><App/></LicenseGate>
20
+ ```
21
+
22
+ ## Membres / config
23
+ Stack : `mosta-licensing-stack`. Pair logique : `@mostajs/licensing`.
24
+ Variables `.env` : `LICENSE_SERVER_URL` (défaut `https://licences.amia.fr`), `LICENSE_PROJECT`,
25
+ `LICENSE_PUBLIC_KEY` (PEM base64).
26
+
27
+ ## Provenance
28
+ Composants extraits : `LicenseGate`, `EnhancedLicenseGate`, `LicenseActivationForm`,
29
+ `SimpleLicenseActivation`, `PendingLicenseActivation`, `LicenseConfigForm` + `lib/license-service`,
30
+ `lib/licenseClient`. **À généraliser** : remplacer les URLs/projets codés en dur par
31
+ `createLicenseUiFromEnv`. Fiche LLM : `llms.txt`.
package/llms.txt ADDED
@@ -0,0 +1,43 @@
1
+ # @mostajs/licensing-ui-react — fiche LLM
2
+ > UI de licence (gate d'accès, activation, configuration) — extraite de MostaGare, compose @mostajs/licensing.
3
+
4
+ - Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
5
+ - Stack: mosta-licensing-stack · Pair logique: @mostajs/licensing
6
+
7
+ ## RÔLE
8
+ Couche de PRÉSENTATION des licences. Composants React EXTRAITS de SolutionCh/MostaGare
9
+ (LicenseGate, EnhancedLicenseGate, LicenseActivationForm, SimpleLicenseActivation,
10
+ PendingLicenseActivation, LicenseConfigForm) + un contrôleur HEADLESS qui COMPOSE
11
+ @mostajs/licensing (DEVRULES §10 : aucune logique de licence réécrite).
12
+
13
+ ## EXPORTS
14
+ - LicenseProvider, useLicenseUi ← contexte alimenté par createLicenseUiFromEnv (config .env)
15
+ - Composants React (GÉNÉRALISÉS : consomment useLicenseUi, plus d'URL/projet en dur) :
16
+ LicenseGate, EnhancedLicenseGate, LicenseActivationForm, SimpleLicenseActivation,
17
+ PendingLicenseActivation, LicenseConfigForm (./react/*)
18
+ - createLicenseUi(opts) -> { client, check, requestCode, activate }
19
+ - createLicenseUiFromEnv(env) / licenseUiConfigFromEnv(env) ← CONFIG PILOTÉE PAR .env
20
+
21
+ ## USAGE REACT
22
+ Entourer l'app de <LicenseProvider env={{ LICENSE_SERVER_URL, LICENSE_PROJECT, LICENSE_PUBLIC_KEY }}>
23
+ puis utiliser <LicenseGate>/<LicenseActivationForm> (qui lisent serverUrl/project via useLicenseUi()).
24
+
25
+ ## CONFIG .env
26
+ - LICENSE_SERVER_URL (défaut https://licences.amia.fr)
27
+ - LICENSE_PROJECT (ex. salsabil)
28
+ - LICENSE_PUBLIC_KEY (clé publique PEM, base64)
29
+
30
+ ## API (contrôleur headless, compose @mostajs/licensing)
31
+ - check(signed) -> { valid, status, reason?, expiresAt } (checkLicense)
32
+ - requestCode({ email, company?, message? }) (POST /api/request-activation-code)
33
+ - activate({ activationCode, machineId, machineName? }) (download + verify + notify)
34
+ - client : le createLicenseClient brut de @mostajs/licensing
35
+
36
+ ## DÉPEND DE (peer)
37
+ - @mostajs/licensing (logique : checkLicense, createLicenseClient), react, lucide-react
38
+
39
+ ## NOTE EXTRACTION
40
+ Composants extraits de MostaGare (`src/components/license/*`, `lib/license-service`, `lib/licenseClient`)
41
+ PUIS GÉNÉRALISÉS : URLs serveur & projet codés en dur remplacés par la config .env — les composants
42
+ React lisent serverUrl/project via useLicenseUi() (<LicenseProvider> ← createLicenseUiFromEnv) ; les
43
+ libs lisent process.env.LICENSE_SERVER_URL / LICENSE_PROJECT (avec repli). Plus aucun 'MostaGare'/'gestigare'.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mostajs/licensing-ui-react",
3
+ "version": "0.1.0",
4
+ "description": "UI de licence (gate d'accès, activation, configuration) — extraite de MostaGare, compose @mostajs/licensing (vérif RSA + client serveur de licences).",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "src/index.ts",
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./react/*": "./src/react/*"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "llms.txt"
17
+ ],
18
+ "keywords": [
19
+ "license",
20
+ "licence",
21
+ "activation",
22
+ "ui",
23
+ "react",
24
+ "mostajs"
25
+ ],
26
+ "peerDependencies": {
27
+ "@mostajs/licensing": ">=0.1.0",
28
+ "react": ">=18"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "react": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "lucide-react": ">=0.300.0"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ // @mostajs/licensing-ui — couche de PRÉSENTATION des licences.
2
+ // Composants React EXTRAITS de SolutionCh/MostaGare (gate, activation, configuration) +
3
+ // contrôleur headless qui COMPOSE @mostajs/licensing (client serveur + vérif RSA). DEVRULES §10.
4
+ // @author Dr Hamid MADANI <drmdh@msn.com>
5
+
6
+ // Contexte de configuration (.env) — les composants le consomment via useLicenseUi().
7
+ export { LicenseProvider, useLicenseUi } from './react/LicenseProvider';
8
+
9
+ // Composants React (extraits de MostaGare) — réutilisables par les apps Next/React.
10
+ export { default as LicenseGate } from './react/LicenseGate';
11
+ export { default as EnhancedLicenseGate } from './react/EnhancedLicenseGate';
12
+ export { default as LicenseActivationForm } from './react/LicenseActivationForm';
13
+ export { default as SimpleLicenseActivation } from './react/SimpleLicenseActivation';
14
+ export { default as PendingLicenseActivation } from './react/PendingLicenseActivation';
15
+ export { default as LicenseConfigForm } from './react/LicenseConfigForm';
16
+
17
+ import { createLicenseClient, checkLicense } from '@mostajs/licensing';
18
+
19
+ export interface LicenseUiOptions {
20
+ /** URL du serveur de licences (ex. https://licences.amia.fr). */
21
+ baseUrl: string;
22
+ /** Projet (ex. 'salsabil', 'MostaGare'). */
23
+ project?: string;
24
+ /** Clé publique PEM pour vérifier les signatures. */
25
+ publicKeyPem?: string;
26
+ /** fetch injectable (défaut: global). */
27
+ fetch?: any;
28
+ }
29
+
30
+ /**
31
+ * Contrôleur headless — COMPOSE @mostajs/licensing (pas de logique réécrite) :
32
+ * client du serveur de licences + vérification de signature/expiration. Les composants
33
+ * React s'y branchent (un seul point d'intégration, testable sans navigateur).
34
+ */
35
+ /**
36
+ * Construit les options depuis l'environnement (.env) — CONFIG PILOTÉE PAR .env :
37
+ * LICENSE_SERVER_URL (défaut https://licences.amia.fr)
38
+ * LICENSE_PROJECT (ex. salsabil)
39
+ * LICENSE_PUBLIC_KEY (clé publique PEM en base64)
40
+ */
41
+ export function licenseUiConfigFromEnv(env: Record<string, string | undefined> = {}): LicenseUiOptions {
42
+ return {
43
+ baseUrl: env.LICENSE_SERVER_URL || 'https://licences.amia.fr',
44
+ project: env.LICENSE_PROJECT || undefined,
45
+ publicKeyPem: env.LICENSE_PUBLIC_KEY ? Buffer.from(env.LICENSE_PUBLIC_KEY, 'base64').toString('utf8') : undefined,
46
+ };
47
+ }
48
+
49
+ /** Raccourci : contrôleur configuré entièrement depuis .env. */
50
+ export function createLicenseUiFromEnv(env: Record<string, string | undefined> = {}) {
51
+ return createLicenseUi(licenseUiConfigFromEnv(env));
52
+ }
53
+
54
+ export function createLicenseUi(opts: LicenseUiOptions) {
55
+ const client = createLicenseClient({ baseUrl: opts.baseUrl, publicKeyPem: opts.publicKeyPem, fetch: opts.fetch });
56
+ return {
57
+ client,
58
+ /** Vérifie une licence signée (signature RSA + projet + expiration). */
59
+ check: (signed: any) => checkLicense(signed, { project: opts.project, publicKeyPem: opts.publicKeyPem ?? '' }),
60
+ /** Demande un code d'activation par email. */
61
+ requestCode: (p: { email: string; company?: string; message?: string }) =>
62
+ client.requestActivationCode({ project: opts.project ?? '', ...p }),
63
+ /** Active une machine : télécharge la licence du code, la vérifie, notifie. */
64
+ activate: (p: { activationCode: string; machineId: string; machineName?: string }) =>
65
+ client.activate({ project: opts.project, ...p }),
66
+ };
67
+ }
@@ -0,0 +1,360 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import os from "os";
5
+
6
+ export interface LicenseInfo {
7
+ key: string;
8
+ email: string;
9
+ project: string;
10
+ expires_at: string;
11
+ activations: Activation[];
12
+ max_activations: number;
13
+ }
14
+
15
+ export interface Activation {
16
+ machine_id: string;
17
+ machine_name: string;
18
+ activated_at: string;
19
+ }
20
+
21
+ export interface LicenseStatus {
22
+ valid: boolean;
23
+ status: 'valid' | 'invalid' | 'expired' | 'error';
24
+ reason?: string;
25
+ message?: string;
26
+ license?: LicenseInfo;
27
+ remaining_days?: number;
28
+ remaining_activations?: number;
29
+ }
30
+
31
+ export class LicenseService {
32
+ private licenseDir: string;
33
+ private publicKeyPath: string;
34
+ private licensePath: string | null = null;
35
+
36
+ constructor() {
37
+ this.licenseDir = path.join(process.cwd(), "licences");
38
+ this.publicKeyPath = path.join(this.licenseDir, "public.pem");
39
+ this.findLicenseFile();
40
+ }
41
+
42
+ /**
43
+ * Trouve le fichier de licence signé
44
+ */
45
+ private findLicenseFile(): void {
46
+ if (fs.existsSync(this.licenseDir)) {
47
+ const files = fs.readdirSync(this.licenseDir);
48
+ const licenseFile = files.find(file => file.endsWith('_licence.signed.json'));
49
+
50
+ if (licenseFile) {
51
+ this.licensePath = path.join(this.licenseDir, licenseFile);
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Obtient l'ID unique de la machine
58
+ */
59
+ public getMachineId(): string {
60
+ const networkInterfaces = os.networkInterfaces();
61
+ const macAddresses: string[] = [];
62
+
63
+ for (const interfaceName in networkInterfaces) {
64
+ const interfaces = networkInterfaces[interfaceName];
65
+ if (interfaces) {
66
+ for (const iface of interfaces) {
67
+ if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
68
+ macAddresses.push(iface.mac);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // Utiliser la première adresse MAC ou un ID basé sur le hostname
75
+ const machineId = macAddresses.length > 0
76
+ ? macAddresses[0].replace(/:/g, '').toUpperCase()
77
+ : crypto.createHash('sha256').update(os.hostname()).digest('hex').substring(0, 12).toUpperCase();
78
+
79
+ return machineId;
80
+ }
81
+
82
+ /**
83
+ * Obtient le nom de la machine
84
+ */
85
+ public getMachineName(): string {
86
+ return os.hostname();
87
+ }
88
+
89
+ /**
90
+ * Vérifie la signature de la licence
91
+ */
92
+ private verifySignature(signedData: any, publicKey: string): boolean {
93
+ try {
94
+ // Vérification temporaire pour les signatures du serveur web
95
+ // Détection générique basée sur le format du code d'activation
96
+ const isWebServerLicense = signedData.license.key &&
97
+ /^[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/.test(signedData.license.key);
98
+
99
+ if (isWebServerLicense) {
100
+ console.log("Code généré par serveur web détecté (format XXXX-XXXX-XXXX-XXXX), validation RSA appliquée");
101
+ // Continuer avec la vérification RSA normale pour les codes du serveur web
102
+ } else if (signedData.signature && signedData.signature.includes(signedData.license.project)) {
103
+ console.log("Signature temporaire détectée, validation bypassed pour développement");
104
+ return true;
105
+ }
106
+
107
+ // Si la licence a des activations, vérifier sans les activations (signature originale)
108
+ const licenseForVerification = { ...signedData.license };
109
+ if (licenseForVerification.activations && licenseForVerification.activations.length > 0) {
110
+ // Créer une copie sans les activations pour vérifier la signature originale
111
+ delete licenseForVerification.activations;
112
+ console.log("Licence avec activations détectée, vérification sans les activations");
113
+ }
114
+
115
+ // Reconstruire le JSON canonique (comme côté Python)
116
+ const sortedKeys = Object.keys(licenseForVerification).sort();
117
+ const sortedLicense: any = {};
118
+ for (const key of sortedKeys) {
119
+ sortedLicense[key] = licenseForVerification[key];
120
+ }
121
+
122
+ const canonicalPayload = JSON.stringify(sortedLicense);
123
+
124
+ // Vérifier la signature
125
+ const verify = crypto.createVerify("RSA-SHA256");
126
+ verify.update(canonicalPayload, "utf8");
127
+ verify.end();
128
+
129
+ const signatureBuffer = Buffer.from(signedData.signature, "hex");
130
+ return verify.verify(publicKey, signatureBuffer);
131
+
132
+ } catch (error) {
133
+ console.error("Erreur vérification signature:", error);
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Vérifie la licence complète
140
+ */
141
+ public async verifyLicense(): Promise<LicenseStatus> {
142
+ try {
143
+ // Vérifier l'existence des fichiers
144
+ if (!this.licensePath || !fs.existsSync(this.licensePath)) {
145
+ return {
146
+ valid: false,
147
+ status: 'invalid',
148
+ reason: 'license_not_found',
149
+ message: 'Aucun fichier de licence trouvé'
150
+ };
151
+ }
152
+
153
+ if (!fs.existsSync(this.publicKeyPath)) {
154
+ return {
155
+ valid: false,
156
+ status: 'invalid',
157
+ reason: 'public_key_not_found',
158
+ message: 'Clé publique non trouvée'
159
+ };
160
+ }
161
+
162
+ // Lire les fichiers
163
+ const signedData = JSON.parse(fs.readFileSync(this.licensePath, "utf8"));
164
+ const publicKey = fs.readFileSync(this.publicKeyPath, "utf8");
165
+
166
+ // Vérifier la structure
167
+ if (!signedData.license || !signedData.signature) {
168
+ return {
169
+ valid: false,
170
+ status: 'invalid',
171
+ reason: 'invalid_format',
172
+ message: 'Format de licence invalide'
173
+ };
174
+ }
175
+
176
+ // Vérifier la signature
177
+ if (!this.verifySignature(signedData, publicKey)) {
178
+ return {
179
+ valid: false,
180
+ status: 'invalid',
181
+ reason: 'invalid_signature',
182
+ message: 'Signature invalide'
183
+ };
184
+ }
185
+
186
+ // Vérifier la date d'expiration
187
+ const now = new Date();
188
+ const expiryDate = new Date(signedData.license.expires_at);
189
+ const timeDiff = expiryDate.getTime() - now.getTime();
190
+ const remainingDays = Math.ceil(timeDiff / (1000 * 3600 * 24));
191
+
192
+ if (now > expiryDate) {
193
+ return {
194
+ valid: false,
195
+ status: 'expired',
196
+ reason: 'license_expired',
197
+ message: `Licence expirée depuis ${-remainingDays} jours`,
198
+ license: signedData.license,
199
+ remaining_days: remainingDays
200
+ };
201
+ }
202
+
203
+ // Vérifier le nombre d'activations
204
+ const activations = signedData.license.activations || [];
205
+ const maxActivations = signedData.license.max_activations || 3;
206
+
207
+ // Vérifier si cette machine est activée
208
+ const machineId = this.getMachineId();
209
+ const isThisMachineActivated = activations.some((a: Activation) => a.machine_id === machineId);
210
+
211
+ if (!isThisMachineActivated && activations.length >= maxActivations) {
212
+ return {
213
+ valid: false,
214
+ status: 'invalid',
215
+ reason: 'max_activations_reached',
216
+ message: `Nombre maximum d'activations atteint (${maxActivations})`,
217
+ license: signedData.license
218
+ };
219
+ }
220
+
221
+ // Licence valide !
222
+ return {
223
+ valid: true,
224
+ status: 'valid',
225
+ message: 'Licence valide et active',
226
+ license: signedData.license,
227
+ remaining_days: remainingDays,
228
+ remaining_activations: maxActivations - activations.length
229
+ };
230
+
231
+ } catch (error: any) {
232
+ return {
233
+ valid: false,
234
+ status: 'error',
235
+ reason: 'verification_error',
236
+ message: error.message || 'Erreur lors de la vérification'
237
+ };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Notifie le serveur qu'une activation a eu lieu
243
+ */
244
+ private async notifyServerActivation(activationCode: string, machineId: string, machineName: string): Promise<void> {
245
+ try {
246
+ const { exec } = require('child_process');
247
+ const util = require('util');
248
+ const execPromise = util.promisify(exec);
249
+
250
+ const curlCommand = `curl -k -X POST ${process.env.LICENSE_SERVER_URL || "https://licences.amia.fr"}/api/notify-activation \
251
+ -H "Content-Type: application/x-www-form-urlencoded" \
252
+ -d "activationCode=${activationCode}&machineId=${machineId}&machineName=${machineName}"`;
253
+
254
+ await execPromise(curlCommand);
255
+ console.log("Activation notifiée au serveur avec succès");
256
+ } catch (error) {
257
+ console.warn("Erreur lors de la notification au serveur:", error);
258
+ throw error;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Active la licence pour cette machine
264
+ */
265
+ public async activateMachine(): Promise<{ success: boolean; message: string; activation?: Activation }> {
266
+ try {
267
+ if (!this.licensePath || !fs.existsSync(this.licensePath)) {
268
+ return {
269
+ success: false,
270
+ message: 'Licence non trouvée'
271
+ };
272
+ }
273
+
274
+ const signedData = JSON.parse(fs.readFileSync(this.licensePath, "utf8"));
275
+ const activations = signedData.license.activations || [];
276
+ const maxActivations = signedData.license.max_activations || 3;
277
+
278
+ const machineId = this.getMachineId();
279
+ const machineName = this.getMachineName();
280
+
281
+ // Vérifier si déjà activée
282
+ const existingActivation = activations.find((a: Activation) => a.machine_id === machineId);
283
+ if (existingActivation) {
284
+ return {
285
+ success: true,
286
+ message: 'Machine déjà activée',
287
+ activation: existingActivation
288
+ };
289
+ }
290
+
291
+ // Vérifier la limite
292
+ if (activations.length >= maxActivations) {
293
+ return {
294
+ success: false,
295
+ message: `Limite d'activations atteinte (${maxActivations})`
296
+ };
297
+ }
298
+
299
+ // Ajouter l'activation
300
+ const newActivation: Activation = {
301
+ machine_id: machineId,
302
+ machine_name: machineName,
303
+ activated_at: new Date().toISOString()
304
+ };
305
+
306
+ activations.push(newActivation);
307
+ signedData.license.activations = activations;
308
+
309
+ // Sauvegarder (Note: invalide la signature)
310
+ fs.writeFileSync(this.licensePath, JSON.stringify(signedData, null, 2));
311
+
312
+ // Notifier le serveur de l'activation
313
+ try {
314
+ const activationCode = signedData.license.key;
315
+ if (activationCode) {
316
+ await this.notifyServerActivation(activationCode, machineId, machineName);
317
+ }
318
+ } catch (error) {
319
+ console.warn("Impossible de notifier le serveur de l'activation:", error);
320
+ // Ne pas faire échouer l'activation pour autant
321
+ }
322
+
323
+ return {
324
+ success: true,
325
+ message: 'Machine activée avec succès',
326
+ activation: newActivation
327
+ };
328
+
329
+ } catch (error: any) {
330
+ return {
331
+ success: false,
332
+ message: error.message || 'Erreur lors de l\'activation'
333
+ };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Obtient les informations de licence de manière sécurisée
339
+ */
340
+ public async getLicenseInfo(): Promise<LicenseInfo | null> {
341
+ const status = await this.verifyLicense();
342
+ return status.license || null;
343
+ }
344
+
345
+ /**
346
+ * Vérifie si la licence est valide et cette machine est activée
347
+ */
348
+ public async isLicenseValidAndActivated(): Promise<boolean> {
349
+ const status = await this.verifyLicense();
350
+ if (!status.valid) return false;
351
+
352
+ const machineId = this.getMachineId();
353
+ const activations = status.license?.activations || [];
354
+
355
+ return activations.some(a => a.machine_id === machineId);
356
+ }
357
+ }
358
+
359
+ // Export une instance singleton
360
+ export const licenseService = new LicenseService();
@@ -0,0 +1,76 @@
1
+ import axios from "axios";
2
+ import os from "os";
3
+ import { hostname } from "os";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import crypto from "crypto";
7
+
8
+ // Config pilotée par .env : LICENSE_SERVER_URL, LICENSE_PROJECT (avec repli paramètre).
9
+ const SERVER_URL = (process.env.LICENSE_SERVER_URL || "https://licences.amia.fr").replace(/\/+$/, "");
10
+ const DEFAULT_PROJECT = process.env.LICENSE_PROJECT || "app";
11
+
12
+ export async function requestLicenseFromClient(email: string, project = DEFAULT_PROJECT, version = "1.0.0") {
13
+ const device_id = crypto.createHash("sha256")
14
+ .update(os.hostname() + os.platform())
15
+ .digest("hex");
16
+
17
+ const data = {
18
+ email,
19
+ project,
20
+ version,
21
+ device_id,
22
+ os_info: `${os.platform()} ${os.release()}`,
23
+ hostname: hostname()
24
+ };
25
+
26
+ try {
27
+ const res = await axios.post(`${SERVER_URL}/api/licenses/generate-from-client`, data, {
28
+ responseType: "blob"
29
+ });
30
+
31
+ const dir = path.join(process.cwd(), "licences");
32
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir);
33
+ const filePath = path.join(dir, `${project}_licence.signed.json`);
34
+ fs.writeFileSync(filePath, res.data);
35
+ return { success: true, filePath };
36
+ } catch (error: any) {
37
+ console.error("❌ Erreur :", error.response?.data || error.message);
38
+ return { success: false, error };
39
+ }
40
+ }
41
+ export async function validateLicense(licensePath: string, pubkeyPath: string) {
42
+ try {
43
+ const license = JSON.parse(fs.readFileSync(licensePath, "utf8"));
44
+ const pubKey = fs.readFileSync(pubkeyPath, "utf8");
45
+
46
+ const verify = crypto.createVerify("SHA256");
47
+ verify.update(JSON.stringify(license.license));
48
+ verify.end();
49
+
50
+ const isValid = verify.verify(pubKey, Buffer.from(license.signature, "hex"));
51
+
52
+ if (isValid) {
53
+ const now = new Date().toISOString();
54
+ if (now < license.license.expires_at) {
55
+ return { valid: true };
56
+ } else {
57
+ return { valid: false, reason: "expired" };
58
+ }
59
+ } else {
60
+ return { valid: false, reason: "invalid_signature" };
61
+ }
62
+ } catch (e) {
63
+ console.error("Erreur licence :", e);
64
+ return { valid: false, reason: "error", error: e };
65
+ }
66
+ }
67
+ export async function checkLicenseValidity() {
68
+ const licensePath = path.join(process.cwd(), "licences", `${DEFAULT_PROJECT}_licence.signed.json`);
69
+ const pubkeyPath = path.join(process.cwd(), "licences", "public.pem");
70
+
71
+ if (!fs.existsSync(licensePath) || !fs.existsSync(pubkeyPath)) {
72
+ return { valid: false, reason: "missing_files" };
73
+ }
74
+
75
+ return await validateLicense(licensePath, pubkeyPath);
76
+ }