@glassnote/client 2.4.7 → 2.4.9

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/INSTALACION.md CHANGED
@@ -108,6 +108,31 @@ sesión de PowerShell, y la app —que la abre el autoarranque— no lo tendría
108
108
  auto-actualización no puede funcionar en esa máquina: hay que reinstalar el cliente
109
109
  (`irm https://glassnote.intermark.ec/install.ps1 | iex`) o instalar Node.
110
110
 
111
+ ## Identidad del dispositivo
112
+
113
+ El uuid con el que el backend reconoce al equipo vive en su propio archivo, `device-id`,
114
+ al lado de los datos de usuario (`%APPDATA%\glassnote` en Windows, `~/.config/glassnote`
115
+ en Linux, `~/Library/Application Support/glassnote` en macOS). Se escribe una vez y no se
116
+ vuelve a tocar.
117
+
118
+ Antes vivía dentro de `.glassnote`, que se reescribe cada vez que cambia cualquier ajuste.
119
+ Bastaba que ese archivo quedara ilegible —un BOM que le metiera cualquier herramienta de
120
+ Windows, un corte de luz a mitad de la escritura— para que la app generara un uuid nuevo:
121
+ el equipo aparecía como uno distinto en el backend y el anterior quedaba huérfano, sin
122
+ rastro de que eran el mismo.
123
+
124
+ Lo que hace ahora al arrancar, en orden:
125
+
126
+ 1. `device-id`, si está;
127
+ 2. si no, lo migra del `.glassnote`, de su `.backup` o del archivo de la versión Godot —
128
+ por eso los equipos que ya están instalados conservan su identidad;
129
+ 3. solo si no queda nada de dónde sacarlo, genera uno nuevo, y lo dice en el log
130
+ (`Sin identidad previa recuperable`).
131
+
132
+ Un `.glassnote` ilegible ya no se sobrescribe: se aparta como `.glassnote.corrupto-<fecha>`
133
+ y se intenta recuperar la configuración del `.backup`. Las escrituras van a un temporal y
134
+ se renombran, así un cierre a destiempo no deja el archivo a medias.
135
+
111
136
  Por `npx` no se actualiza nada: la caché es efímera y npx ya baja la última versión cada
112
137
  vez. Tampoco se registra el arranque automático — apuntaría a una carpeta que se limpia
113
138
  sola.
package/autoStart.js CHANGED
@@ -220,35 +220,6 @@ class AutoStart {
220
220
  }
221
221
  }
222
222
 
223
- // Remove existing plist if it exists
224
- if (fs.existsSync(plistPath)) {
225
- try {
226
- // Use spawn instead of execSync to avoid blocking
227
- const unloadProcess = spawn('launchctl', ['unload', plistPath], {
228
- stdio: 'ignore',
229
- timeout: 5000 // 5 second timeout
230
- });
231
-
232
- unloadProcess.on('error', (error) => {
233
- });
234
-
235
- unloadProcess.on('exit', (code) => {
236
- if (code !== 0) {
237
- console.warn('launchctl unload exited with code:', code, '(may not be loaded)');
238
- }
239
- });
240
- } catch (error) {
241
- console.error('Could not unload existing plist:', error.message);
242
- }
243
-
244
- // Remove the plist file regardless of launchctl status
245
- try {
246
- fs.unlinkSync(plistPath);
247
- } catch (error) {
248
- console.error('Could not remove plist file:', error.message);
249
- }
250
- }
251
-
252
223
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
253
224
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
254
225
  <plist version="1.0">
@@ -273,29 +244,30 @@ ${launchArgs.map((a) => ` <string>${a}</string>`).join('\n')}
273
244
  fs.mkdirSync(plistDir, { recursive: true });
274
245
  }
275
246
 
276
- fs.writeFileSync(plistPath, plistContent);
277
-
278
- // Load the launch agent asynchronously
247
+ // El plist NO se borra ni se recarga si ya dice lo mismo, y NUNCA se hace
248
+ // `launchctl unload`.
249
+ //
250
+ // Esto lo llama el main en CADA arranque. En macOS, cuando el que arranca es el
251
+ // propio LaunchAgent (o sea: al iniciar sesión, que es el caso normal), la app era
252
+ // el job de launchd: el `launchctl unload` de aquí abajo mataba el job, y con él a
253
+ // la app que lo estaba ejecutando. Medido: el cliente se abría al login y se moría
254
+ // a los ~3 segundos, antes incluso del primer chequeo de actualización, que es a
255
+ // los 5. Encima el agente quedaba descargado, así que ni `launchctl list` lo veía.
256
+ //
257
+ // Tampoco se hace `load` cuando el contenido cambia: `RunAtLoad` arrancaría OTRA
258
+ // instancia encima de esta —la app no tiene lock de instancia única— y el archivo
259
+ // nuevo lo lee launchd solo en el próximo login, que es justo cuando tiene que
260
+ // valer. Escribirlo es todo lo que hace falta.
261
+ let actual = null;
279
262
  try {
280
- const loadProcess = spawn('launchctl', ['load', plistPath], {
281
- stdio: 'ignore',
282
- timeout: 5000 // 5 second timeout
283
- });
284
-
285
- loadProcess.on('error', (error) => {
286
- console.error('Error spawning launchctl load:', error.message);
287
- });
288
-
289
- loadProcess.on('exit', (code) => {
290
- if (code !== 0) {
291
- console.warn('launchctl load exited with code:', code);
292
- } else {
293
- console.warn('macOS auto-start configured:', plistPath);
294
- }
295
- });
263
+ actual = fs.readFileSync(plistPath, 'utf8');
296
264
  } catch (error) {
297
- console.error('Error loading launch agent:', error.message);
265
+ actual = null; // no existía
298
266
  }
267
+ if (actual === plistContent) return true;
268
+
269
+ fs.writeFileSync(plistPath, plistContent);
270
+ console.log('macOS auto-start configured:', plistPath);
299
271
 
300
272
  return true;
301
273
  } catch (error) {
package/bin/glassnote.js CHANGED
@@ -52,14 +52,26 @@ function npmEnv() {
52
52
  // con otro entorno, y ahí `npm` no existe. La auto-actualización moría en "npm no se
53
53
  // reconoce como un comando" y nadie lo veía. Si no hay forma de resolver el npm-cli.js se
54
54
  // vuelve al shim de siempre.
55
- function runNpm(args, opts) {
55
+ // Separado de runNpm a propósito: decidir CÓMO se llama a npm es una función pura, y por
56
+ // eso se puede comprobar en cualquier sistema (scripts/test-update.js lo hace). Ejecutarlo
57
+ // no. La regresión que hay que impedir es volver a `shell: true` con un array de
58
+ // argumentos: Node no entrecomilla en ese caso y en Windows la ruta del prefix se parte en
59
+ // el primer espacio —`C:\Users\Santiago Andrade\...`—, con lo que npm intenta un
60
+ // mkdir de 'C:\Users\Santiago' y la instalación entera falla con EPERM.
61
+ function npmInvocation(args) {
56
62
  const nodeBinary = mode.resolveNodeBinary(process.execPath);
57
63
  const npmCli = mode.resolveNpmCli(nodeBinary);
58
- const base = { stdio: 'inherit', shell: false, env: npmEnv() };
59
64
  if (nodeBinary && npmCli) {
60
- return spawnSync(nodeBinary, [npmCli].concat(args), Object.assign(base, opts || {}));
65
+ return { file: nodeBinary, args: [npmCli].concat(args), shell: false, viaNode: true };
61
66
  }
62
- return spawnSync(NPM_CMD, args, Object.assign(base, { shell: IS_WIN }, opts || {}));
67
+ // Sin npm-cli.js localizable no queda más que el shim del PATH. Sigue sin shell:
68
+ // spawnSync con un array ya entrecomilla por su cuenta cuando no hay shell de por medio.
69
+ return { file: NPM_CMD, args, shell: false, viaNode: false };
70
+ }
71
+
72
+ function runNpm(args, opts) {
73
+ const { file, args: argv, shell } = npmInvocation(args);
74
+ return spawnSync(file, argv, Object.assign({ stdio: 'inherit', shell, env: npmEnv() }, opts || {}));
63
75
  }
64
76
 
65
77
  // userData tira de `uuid`, y este CLI corre también donde el árbol de dependencias no
@@ -772,4 +784,10 @@ function main() {
772
784
  }
773
785
  }
774
786
 
775
- process.exit(main());
787
+ // Requerido desde un test, el archivo NO debe correr nada: solo expone las piezas que
788
+ // se comprueban. Como CLI se comporta igual que siempre.
789
+ if (require.main === module) {
790
+ process.exit(main());
791
+ } else {
792
+ module.exports = { npmInvocation, npmEnv, toSpec };
793
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassnote/client",
3
- "version": "2.4.7",
3
+ "version": "2.4.9",
4
4
  "description": "GlassNote — cliente de escritorio de notas superpuestas. Se instala con npx, sin instaladores nativos ni firmas.",
5
5
  "main": "main.js",
6
6
  "publishConfig": {
@@ -51,7 +51,9 @@
51
51
  "sync-installers": "node scripts/sync-installers.js",
52
52
  "local-registry": "node scripts/local-registry.js",
53
53
  "prepublishOnly": "node scripts/check-publish.js",
54
- "start-electron": "electron ."
54
+ "start-electron": "electron .",
55
+ "test-update": "node scripts/test-update.js",
56
+ "test": "node scripts/test-update.js"
55
57
  },
56
58
  "keywords": [
57
59
  "glassnote",
package/userData.js CHANGED
@@ -1,492 +1,437 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { v4: uuidv4 } = require('uuid');
4
-
5
- // Logging de diagnóstico para problema del UUID
6
-
7
- const userDataDir = path.join(
8
- process.env.APPDATA ||
9
- (process.platform === 'darwin'
10
- ? path.join(process.env.HOME, 'Library', 'Application Support')
11
- : path.join(process.env.HOME, '.config')),
12
- 'glassnote'
13
- );
14
-
15
- const userDataPath = path.join(userDataDir, '.glassnote');
16
-
17
- // Path to Godot version's data file
18
- // Get the base app data directory (parent of userDataDir)
19
- const baseAppDataDir = path.dirname(userDataDir);
20
- const godotDataPath = path.join(
21
- userDataDir,
22
- 'Godot',
23
- 'app_userdata',
24
- 'glassnote',
25
- '.glassnote'
26
- );
27
-
28
-
29
- // In-memory cache for user data
30
- let userDataCache = null;
31
-
32
- function ensureFileExists() {
33
-
34
- if (!fs.existsSync(userDataDir)) {
35
- fs.mkdirSync(userDataDir, { recursive: true });
36
- }
37
-
38
- if (!fs.existsSync(userDataPath)) {
39
- let uuid = uuidv4(); // Default to new UUID
40
- let servers = []; // Default empty servers array
41
-
42
- // Check if Godot version's data file exists and try to migrate data
43
- if (fs.existsSync(godotDataPath)) {
44
- try {
45
- const godotData = fs.readFileSync(godotDataPath, 'utf8');
46
- const parsedData = JSON.parse(godotData);
47
-
48
- // Migrate UUID if available
49
- if (parsedData.uuid) {
50
- uuid = parsedData.uuid;
51
- }
52
-
53
- // Migrate server value to servers array if available
54
- if (parsedData.server) {
55
- servers = [parsedData.server];
56
- }
57
- } catch (err) {
58
- console.error('Error reading Godot data file:', err);
59
- console.error('Generating new UUID due to error reading Godot data');
60
- }
61
- } else {
62
- console.log('No Godot data file found, generating new UUID:', uuid);
63
- }
64
-
65
- const initialData = { uuid: uuid, servers: servers };
66
- fs.writeFileSync(userDataPath, JSON.stringify(initialData, null, 2));
67
- userDataCache = initialData;
68
- return initialData;
69
- }
70
-
71
- // Load data into cache if not already loaded
72
- if (userDataCache === null) {
73
- userDataCache = readData();
74
- } else {
75
- console.log('Cache already loaded, skipping readData()');
76
- }
77
-
78
- // Validate that UUID exists and is a valid format
79
-
80
- if (!userDataCache.uuid || typeof userDataCache.uuid !== 'string' || userDataCache.uuid.trim() === '' || !isValidUUID(userDataCache.uuid)) {
81
-
82
- const newUUID = uuidv4();
83
- userDataCache.uuid = newUUID;
84
- writeData(userDataCache);
85
- } else {
86
- console.log('UUID validation PASSED');
87
- }
88
-
89
- return userDataCache;
90
- }
91
-
92
- function readData() {
93
-
94
- try {
95
- const data = fs.readFileSync(userDataPath, 'utf8');
96
-
97
- const parsedData = JSON.parse(data);
98
-
99
- // Ensure we always return an object with proper structure
100
- if (typeof parsedData !== 'object' || parsedData === null) {
101
- console.error('Invalid data format, returning default structure');
102
- userDataCache = { uuid: null, servers: [] };
103
- return userDataCache;
104
- }
105
-
106
- // Ensure servers array exists
107
- if (!Array.isArray(parsedData.servers)) {
108
- if (typeof parsedData.servers === 'string') {
109
- parsedData.servers = [parsedData.servers];
110
- } else {
111
- parsedData.servers = [];
112
- }
113
- } else {
114
- console.log('servers is already array, length:', parsedData.servers.length);
115
- }
116
-
117
- userDataCache = parsedData;
118
- return userDataCache;
119
- } catch (err) {
120
- console.error('Error reading user data:', err.message);
121
- console.error('Error stack:', err.stack);
122
- // Return a properly structured object instead of empty
123
- userDataCache = { uuid: null, servers: [] };
124
- console.error('Returning error structure:', userDataCache);
125
- console.error('=== readData() returning (error) ===');
126
- return userDataCache;
127
- }
128
- }
129
-
130
- function writeData(data) {
131
-
132
- // Validate data structure before writing
133
- if (!data || typeof data !== 'object') {
134
- console.error('writeData() called with invalid data:', data);
135
- return false;
136
- }
137
-
138
- // ALWAYS preserve a valid UUID - never overwrite with invalid/null UUID
139
- let originalUUID = data.uuid;
140
- let needsUUIDPreservation = false;
141
-
142
- // Check if UUID in data is valid
143
- if (!data.uuid || typeof data.uuid !== 'string' || !isValidUUID(data.uuid)) {
144
- console.warn('writeData(): UUID in data is invalid:', data.uuid);
145
- needsUUIDPreservation = true;
146
- }
147
-
148
- // If UUID needs preservation, read current file
149
- if (needsUUIDPreservation) {
150
- try {
151
- if (fs.existsSync(userDataPath)) {
152
- const currentContent = fs.readFileSync(userDataPath, 'utf8');
153
- const currentData = JSON.parse(currentContent);
154
- if (currentData.uuid && isValidUUID(currentData.uuid)) {
155
- data.uuid = currentData.uuid;
156
- } else {
157
- console.warn('writeData(): No valid UUID in current file either');
158
- // If no valid UUID exists anywhere, generate a new one
159
- if (!data.uuid || !isValidUUID(data.uuid)) {
160
- const newUUID = uuidv4();
161
- data.uuid = newUUID;
162
- }
163
- }
164
- } else {
165
- // File doesn't exist, ensure we have a valid UUID
166
- if (!data.uuid || !isValidUUID(data.uuid)) {
167
- const newUUID = uuidv4();
168
- data.uuid = newUUID;
169
- }
170
- }
171
- } catch (err) {
172
- console.error('writeData(): Error reading current file:', err.message);
173
- // On error, ensure we have a valid UUID
174
- if (!data.uuid || !isValidUUID(data.uuid)) {
175
- const newUUID = uuidv4();
176
- console.error('writeData(): Generating new UUID after error:', newUUID);
177
- data.uuid = newUUID;
178
- }
179
- }
180
- } else {
181
- console.log('writeData(): UUID in data is valid:', data.uuid);
182
- }
183
-
184
- // Create backup before writing
185
- try {
186
- if (fs.existsSync(userDataPath)) {
187
- const backupPath = userDataPath + '.backup';
188
- fs.copyFileSync(userDataPath, backupPath);
189
- }
190
- } catch (err) {
191
- console.warn('writeData(): Could not create backup:', err.message);
192
- }
193
-
194
- try {
195
- fs.writeFileSync(userDataPath, JSON.stringify(data, null, 2));
196
- return true;
197
- } catch (err) {
198
- console.error('Error writing user data:', err.message);
199
- console.error('Error stack:', err.stack);
200
- return false;
201
- }
202
- }
203
-
204
- function get(key, nestedKey) {
205
- // Handle both get(key) and get(key, nestedKey)
206
- if (arguments.length === 1) {
207
- nestedKey = undefined;
208
- }
209
-
210
- // Ensure cache is loaded
211
- if (userDataCache === null) {
212
- ensureFileExists();
213
- }
214
-
215
- if (nestedKey !== undefined) {
216
- // If nestedKey is provided, get nested value
217
- if (userDataCache[key] && typeof userDataCache[key] === 'object' && userDataCache[key] !== null) {
218
- return userDataCache[key][nestedKey];
219
- }
220
- return undefined;
221
- } else {
222
- // If only key is provided, get top-level value
223
- return userDataCache[key];
224
- }
225
- }
226
-
227
- function set(key, nestedKey, value) {
228
-
229
- // Handle both set(key, value) and set(key, nestedKey, value)
230
- if (arguments.length === 2) {
231
- // set(key, value) - value is actually the second argument
232
- value = nestedKey;
233
- nestedKey = undefined;
234
- }
235
-
236
- // BLOCK ATTEMPTS TO OVERWRITE UUID FROM RENDERER
237
- if (key === 'uuid' && nestedKey === undefined) {
238
- console.error('SECURITY: Attempt to overwrite UUID blocked!');
239
- console.error('UUID can only be set by Electron main process, not from renderer.');
240
-
241
- // Get current UUID to return success but don't actually change it
242
- if (userDataCache === null) {
243
- ensureFileExists();
244
- }
245
-
246
- // CRITICAL FIX: DO NOT update cache with invalid UUID from renderer
247
- // Keep the existing UUID in cache to prevent ensureFileExists() from regenerating it
248
-
249
- // Return true to avoid breaking renderer expectations, but UUID is not actually changed
250
- return true;
251
- }
252
-
253
- // Ensure cache is loaded
254
- if (userDataCache === null) {
255
- ensureFileExists();
256
- }
257
-
258
- // Validate cache structure
259
- if (!userDataCache || typeof userDataCache !== 'object') {
260
- console.error('set(): userDataCache is invalid, reinitializing');
261
- userDataCache = { uuid: null, servers: [] };
262
- }
263
-
264
- // Preserve UUID if it exists but cache structure is incomplete
265
- if (!userDataCache.uuid || typeof userDataCache.uuid !== 'string') {
266
- try {
267
- if (fs.existsSync(userDataPath)) {
268
- const currentContent = fs.readFileSync(userDataPath, 'utf8');
269
- const currentData = JSON.parse(currentContent);
270
- if (currentData.uuid && typeof currentData.uuid === 'string') {
271
- userDataCache.uuid = currentData.uuid;
272
- }
273
- }
274
- } catch (err) {
275
- console.error('set(): Error reading file for UUID:', err.message);
276
- }
277
- }
278
-
279
- // Handle case where nestedKey is an object but should be treated as value
280
- // This happens when: set(key, object, undefined) or set(key, object, null)
281
- if (nestedKey !== undefined && typeof nestedKey === 'object' &&
282
- (value === undefined || value === null)) {
283
- // Swap: value = nestedKey, nestedKey = undefined
284
- value = nestedKey;
285
- nestedKey = undefined;
286
- }
287
-
288
- // Also handle case where nestedKey is not a string (invalid for nested key)
289
- if (nestedKey !== undefined && typeof nestedKey !== 'string') {
290
- // If nestedKey is not a string, it can't be used as an object key
291
- // Treat it as value instead
292
- value = nestedKey;
293
- nestedKey = undefined;
294
- }
295
-
296
- if (nestedKey !== undefined) {
297
- // If nestedKey is provided, set nested value
298
- // Check if current value is an object, if not, replace it with an object
299
- if (!userDataCache[key] || typeof userDataCache[key] !== 'object' || Array.isArray(userDataCache[key])) {
300
- userDataCache[key] = {};
301
- }
302
- userDataCache[key][nestedKey] = value;
303
- } else {
304
- // If only key is provided, set top-level value
305
- userDataCache[key] = value;
306
- }
307
-
308
- return writeData(userDataCache);
309
- }
310
-
311
- function remove(key, nestedKey) {
312
- // Handle both remove(key) and remove(key, nestedKey)
313
- if (arguments.length === 1) {
314
- nestedKey = undefined;
315
- }
316
-
317
- // Ensure cache is loaded
318
- if (userDataCache === null) {
319
- ensureFileExists();
320
- }
321
-
322
- if (nestedKey !== undefined) {
323
- // If nestedKey is provided, remove nested value
324
- if (userDataCache[key] && typeof userDataCache[key] === 'object' && userDataCache[key] !== null) {
325
- delete userDataCache[key][nestedKey];
326
- // If the object becomes empty, remove the entire key
327
- if (Object.keys(userDataCache[key]).length === 0) {
328
- delete userDataCache[key];
329
- }
330
- }
331
- } else {
332
- // If only key is provided, remove top-level value
333
- delete userDataCache[key];
334
- }
335
-
336
- return writeData(userDataCache);
337
- }
338
-
339
- function getUUID() {
340
-
341
- // If UUID is already in cache and valid, return it immediately
342
- if (userDataCache && userDataCache.uuid && typeof userDataCache.uuid === 'string' && isValidUUID(userDataCache.uuid)) {
343
- return userDataCache.uuid;
344
- }
345
-
346
- // Otherwise, ensure file exists and get UUID
347
- const data = ensureFileExists();
348
-
349
- // Double-check that UUID is valid
350
- if (!data.uuid || typeof data.uuid !== 'string' || !isValidUUID(data.uuid)) {
351
- console.error('getUUID(): Invalid UUID returned from ensureFileExists():', data.uuid);
352
- // Try to read directly from file as last resort
353
- try {
354
- if (fs.existsSync(userDataPath)) {
355
- const fileContent = fs.readFileSync(userDataPath, 'utf8');
356
- const fileData = JSON.parse(fileContent);
357
- if (fileData.uuid && isValidUUID(fileData.uuid)) {
358
- // Update cache
359
- userDataCache.uuid = fileData.uuid;
360
- writeData(userDataCache);
361
- return fileData.uuid;
362
- }
363
- }
364
- } catch (err) {
365
- console.error('getUUID(): Error reading UUID from file:', err.message);
366
- }
367
-
368
- // Generate new UUID as last resort
369
- const newUUID = uuidv4();
370
- userDataCache.uuid = newUUID;
371
- writeData(userDataCache);
372
- return newUUID;
373
- }
374
-
375
- return data.uuid;
376
- }
377
-
378
- // Función específica para leer el lock desde el archivo (no del cache)
379
- function getLockFromFile() {
380
- try {
381
- // Leer directamente del archivo, no del cache
382
- if (!fs.existsSync(userDataPath)) {
383
- return undefined;
384
- }
385
-
386
- const data = fs.readFileSync(userDataPath, 'utf8');
387
- const parsedData = JSON.parse(data);
388
-
389
- // Retornar el valor del lock si existe
390
- return parsedData.lock;
391
- } catch (err) {
392
- console.error('Error reading lock from file:', err);
393
- return undefined;
394
- }
395
- }
396
-
397
- function getUserDataPath() {
398
- return userDataDir;
399
- }
400
-
401
- function getUserDataDir() {
402
- return userDataDir;
403
- }
404
-
405
- // Función auxiliar para validar formato UUID
406
- function isValidUUID(uuid) {
407
- if (!uuid || typeof uuid !== 'string') return false;
408
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
409
- return uuidRegex.test(uuid);
410
- }
411
-
412
- // Función para validar y reparar datos de usuario
413
- function validateAndRepairUserData() {
414
-
415
- try {
416
- if (!fs.existsSync(userDataPath)) {
417
- return ensureFileExists();
418
- }
419
-
420
- // Read current file
421
- const fileContent = fs.readFileSync(userDataPath, 'utf8');
422
- const fileData = JSON.parse(fileContent);
423
-
424
- let needsRepair = false;
425
-
426
- // Validate UUID
427
- if (!fileData.uuid || typeof fileData.uuid !== 'string' || !isValidUUID(fileData.uuid)) {
428
- console.warn('Invalid UUID in file, repairing...');
429
- needsRepair = true;
430
-
431
- // Try to preserve existing UUID if format is close
432
- if (fileData.uuid && typeof fileData.uuid === 'string' && fileData.uuid.trim() !== '') {
433
- // Check if it's a UUID without dashes or with wrong format
434
- const cleanUUID = fileData.uuid.trim().toLowerCase();
435
- if (cleanUUID.length === 32 && /^[0-9a-f]{32}$/.test(cleanUUID)) {
436
- // Convert 32-char hex to UUID format
437
- fileData.uuid = cleanUUID.replace(/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/, '$1-$2-$3-$4-$5');
438
- } else {
439
- // Generate new UUID
440
- fileData.uuid = uuidv4();
441
- }
442
- } else {
443
- // Generate new UUID
444
- fileData.uuid = uuidv4();
445
- }
446
- }
447
-
448
- // Ensure servers array exists
449
- if (!Array.isArray(fileData.servers)) {
450
- console.warn('Invalid servers format, repairing...');
451
- needsRepair = true;
452
- if (typeof fileData.servers === 'string') {
453
- fileData.servers = [fileData.servers];
454
- } else {
455
- fileData.servers = [];
456
- }
457
- }
458
-
459
- // Update cache
460
- userDataCache = fileData;
461
-
462
- if (needsRepair) {
463
- writeData(fileData);
464
- } else {
465
- console.log('Data validation passed, no repairs needed');
466
- }
467
-
468
- return fileData;
469
- } catch (err) {
470
- console.error('Error validating user data:', err.message);
471
- console.error('Falling back to ensureFileExists()');
472
- return ensureFileExists();
473
- }
474
- }
475
-
476
- // NOT calling ensureFileExists() here anymore - will be called on demand
477
- // This prevents double initialization and potential UUID regeneration
478
-
479
- module.exports = {
480
- ensureFileExists,
481
- readData,
482
- writeData,
483
- get,
484
- set,
485
- remove,
486
- getUUID,
487
- getLockFromFile,
488
- getUserDataPath,
489
- getUserDataDir,
490
- isValidUUID,
491
- validateAndRepairUserData,
492
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { v4: uuidv4 } = require('uuid');
4
+
5
+ // Logging de diagnóstico para problema del UUID
6
+
7
+ const userDataDir = path.join(
8
+ process.env.APPDATA ||
9
+ (process.platform === 'darwin'
10
+ ? path.join(process.env.HOME, 'Library', 'Application Support')
11
+ : path.join(process.env.HOME, '.config')),
12
+ 'glassnote'
13
+ );
14
+
15
+ const userDataPath = path.join(userDataDir, '.glassnote');
16
+
17
+ // Path to Godot version's data file
18
+ // Get the base app data directory (parent of userDataDir)
19
+ const baseAppDataDir = path.dirname(userDataDir);
20
+ const godotDataPath = path.join(
21
+ userDataDir,
22
+ 'Godot',
23
+ 'app_userdata',
24
+ 'glassnote',
25
+ '.glassnote'
26
+ );
27
+
28
+
29
+ // In-memory cache for user data
30
+ let userDataCache = null;
31
+
32
+ // ── Identidad del dispositivo ───────────────────────────────────────────────────────
33
+ //
34
+ // El uuid vive en SU PROPIO archivo (`device-id`), que se escribe una vez y no se vuelve
35
+ // a tocar. Antes vivía dentro de `.glassnote`, que se reescribe cada vez que cambia
36
+ // cualquier ajuste: bastaba que ese archivo quedara ilegible —un BOM que le metiera
37
+ // cualquier herramienta de Windows, un corte de luz a mitad de la escritura— para que la
38
+ // app generara un uuid nuevo y el backend viera un equipo distinto, sin forma de saber
39
+ // que era el mismo. Separarlo convierte «se corrompió la configuración», que es
40
+ // recuperable, en algo que ya no arrastra la identidad.
41
+ const deviceIdPath = path.join(userDataDir, 'device-id');
42
+ const backupPath = userDataPath + '.backup';
43
+
44
+ let deviceIdCache = null;
45
+
46
+ // El BOM son tres bytes (EF BB BF) que Windows antepone al UTF-8 y que JSON.parse no
47
+ // tolera: el archivo se ve perfecto al abrirlo y revienta igual. Se quita antes de parsear.
48
+ function stripBom(text) {
49
+ return typeof text === 'string' && text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
50
+ }
51
+
52
+ // Escribir directo sobre el archivo real lo deja truncado si el proceso muere a mitad, y
53
+ // un archivo truncado es justo cómo se llegaba a perder la identidad. Al temporal y
54
+ // rename, que es atómico.
55
+ function writeAtomic(file, content) {
56
+ const tmp = `${file}.tmp-${process.pid}`;
57
+ fs.writeFileSync(tmp, content);
58
+ fs.renameSync(tmp, file);
59
+ }
60
+
61
+ // null = el archivo NO existe. Caso legítimo: primer arranque.
62
+ // lanza = existe pero no se pudo leer o parsear.
63
+ // Confundir los dos casos era el bug: un archivo ilegible se trataba como «no hay datos»
64
+ // y de ahí salía un uuid nuevo.
65
+ function loadFile(file) {
66
+ if (!fs.existsSync(file)) return null;
67
+ const parsed = JSON.parse(stripBom(fs.readFileSync(file, 'utf8')));
68
+ if (typeof parsed !== 'object' || parsed === null) {
69
+ throw new Error('el contenido no es un objeto');
70
+ }
71
+ return parsed;
72
+ }
73
+
74
+ // El ilegible se aparta con fecha en vez de sobrescribirse: sin esto no queda rastro de
75
+ // qué pasó ni forma de recuperarlo a mano.
76
+ function apartarIlegible(file) {
77
+ try {
78
+ if (!fs.existsSync(file)) return null;
79
+ const destino = `${file}.corrupto-${new Date().toISOString().replace(/[:.]/g, '-')}`;
80
+ fs.renameSync(file, destino);
81
+ console.error(`Archivo ilegible apartado en ${destino}`);
82
+ return destino;
83
+ } catch (error) {
84
+ console.error('No se pudo apartar el archivo ilegible:', error.message);
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function readDeviceId() {
90
+ try {
91
+ const raw = stripBom(fs.readFileSync(deviceIdPath, 'utf8')).trim();
92
+ return isValidUUID(raw) ? raw : null;
93
+ } catch (error) {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ function writeDeviceId(uuid) {
99
+ try {
100
+ fs.mkdirSync(userDataDir, { recursive: true });
101
+ writeAtomic(deviceIdPath, `${uuid}\n`);
102
+ return true;
103
+ } catch (error) {
104
+ console.error('No se pudo guardar device-id:', error.message);
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // El uuid del equipo, por orden de confianza. Es el ÚNICO sitio del archivo donde se
110
+ // genera uno nuevo: antes había cuatro repartidos y cualquiera podía disparar.
111
+ function deviceUUID() {
112
+ if (deviceIdCache) return deviceIdCache;
113
+
114
+ const guardado = readDeviceId();
115
+ if (guardado) {
116
+ deviceIdCache = guardado;
117
+ return guardado;
118
+ }
119
+
120
+ // Migración de lo que ya está instalado: hasta ahora el uuid vivía en la config, y
121
+ // antes de eso en el archivo de la versión Godot.
122
+ for (const origen of [userDataPath, backupPath, godotDataPath]) {
123
+ let datos = null;
124
+ try {
125
+ datos = loadFile(origen);
126
+ } catch (error) {
127
+ continue; // ilegible: se prueba el siguiente
128
+ }
129
+ if (datos && isValidUUID(datos.uuid)) {
130
+ writeDeviceId(datos.uuid);
131
+ console.log(`device-id creado a partir de ${origen}`);
132
+ deviceIdCache = datos.uuid;
133
+ return datos.uuid;
134
+ }
135
+ }
136
+
137
+ // Primer arranque de verdad, o no quedó nada de dónde sacarlo. Se avisa fuerte: es
138
+ // el único camino por el que un equipo cambia de identidad.
139
+ const nuevo = uuidv4();
140
+ console.warn(`Sin identidad previa recuperable: se genera un uuid nuevo (${nuevo})`);
141
+ writeDeviceId(nuevo);
142
+ deviceIdCache = nuevo;
143
+ return nuevo;
144
+ }
145
+
146
+ // Config de primer arranque. El uuid NO se decide acá: lo pone deviceUUID().
147
+ function datosIniciales() {
148
+ let servers = [];
149
+ try {
150
+ const godot = loadFile(godotDataPath);
151
+ if (godot && godot.server) servers = [godot.server];
152
+ } catch (error) {
153
+ console.error('No se pudo migrar la config de la versión Godot:', error.message);
154
+ }
155
+ return { uuid: null, servers };
156
+ }
157
+
158
+ function ensureFileExists() {
159
+ if (!fs.existsSync(userDataDir)) {
160
+ fs.mkdirSync(userDataDir, { recursive: true });
161
+ }
162
+
163
+ // La identidad se resuelve aparte de la configuración, y antes que ella.
164
+ const uuid = deviceUUID();
165
+
166
+ if (userDataCache === null) {
167
+ userDataCache = readData();
168
+ }
169
+
170
+ if (userDataCache.uuid !== uuid) {
171
+ userDataCache.uuid = uuid;
172
+ writeData(userDataCache);
173
+ }
174
+
175
+ return userDataCache;
176
+ }
177
+
178
+ function readData() {
179
+ let datos = null;
180
+ try {
181
+ datos = loadFile(userDataPath);
182
+ } catch (error) {
183
+ console.error(`No se pudo leer ${userDataPath}: ${error.message}`);
184
+ // El backup se venía escribiendo desde siempre y no lo leía NADIE: una red
185
+ // tendida y sin conectar. Antes de dar la configuración por perdida, se prueba.
186
+ try {
187
+ datos = loadFile(backupPath);
188
+ if (datos) console.warn('Configuración recuperada del backup.');
189
+ } catch (errorBackup) {
190
+ console.error(`El backup tampoco sirve: ${errorBackup.message}`);
191
+ datos = null;
192
+ }
193
+ apartarIlegible(userDataPath);
194
+ }
195
+
196
+ if (datos === null) datos = datosIniciales();
197
+
198
+ if (!Array.isArray(datos.servers)) {
199
+ datos.servers = typeof datos.servers === 'string' ? [datos.servers] : [];
200
+ }
201
+
202
+ userDataCache = datos;
203
+ return userDataCache;
204
+ }
205
+
206
+ function writeData(data) {
207
+ if (!data || typeof data !== 'object') {
208
+ console.error('writeData(): datos inválidos:', data);
209
+ return false;
210
+ }
211
+
212
+ // El uuid ya no se negocia acá: manda device-id. La maquinaria que había antes para
213
+ // «preservar el uuid leyendo el archivo actual» no llegaba a correr nunca, porque
214
+ // quien llamaba ya venía con uno recién inventado en la mano.
215
+ data.uuid = deviceUUID();
216
+
217
+ // El backup se hace SOLO desde un archivo que se puede leer. Copiarlo a ciegas
218
+ // destruía la única copia buena justo cuando hacía falta.
219
+ try {
220
+ if (loadFile(userDataPath)) fs.copyFileSync(userDataPath, backupPath);
221
+ } catch (error) {
222
+ console.warn('El archivo actual no es legible: se conserva el backup anterior.');
223
+ }
224
+
225
+ try {
226
+ writeAtomic(userDataPath, JSON.stringify(data, null, 2));
227
+ return true;
228
+ } catch (error) {
229
+ console.error('Error escribiendo los datos de usuario:', error.message);
230
+ return false;
231
+ }
232
+ }
233
+
234
+ function get(key, nestedKey) {
235
+ // Handle both get(key) and get(key, nestedKey)
236
+ if (arguments.length === 1) {
237
+ nestedKey = undefined;
238
+ }
239
+
240
+ // Ensure cache is loaded
241
+ if (userDataCache === null) {
242
+ ensureFileExists();
243
+ }
244
+
245
+ if (nestedKey !== undefined) {
246
+ // If nestedKey is provided, get nested value
247
+ if (userDataCache[key] && typeof userDataCache[key] === 'object' && userDataCache[key] !== null) {
248
+ return userDataCache[key][nestedKey];
249
+ }
250
+ return undefined;
251
+ } else {
252
+ // If only key is provided, get top-level value
253
+ return userDataCache[key];
254
+ }
255
+ }
256
+
257
+ function set(key, nestedKey, value) {
258
+
259
+ // Handle both set(key, value) and set(key, nestedKey, value)
260
+ if (arguments.length === 2) {
261
+ // set(key, value) - value is actually the second argument
262
+ value = nestedKey;
263
+ nestedKey = undefined;
264
+ }
265
+
266
+ // BLOCK ATTEMPTS TO OVERWRITE UUID FROM RENDERER
267
+ if (key === 'uuid' && nestedKey === undefined) {
268
+ console.error('SECURITY: Attempt to overwrite UUID blocked!');
269
+ console.error('UUID can only be set by Electron main process, not from renderer.');
270
+
271
+ // Get current UUID to return success but don't actually change it
272
+ if (userDataCache === null) {
273
+ ensureFileExists();
274
+ }
275
+
276
+ // CRITICAL FIX: DO NOT update cache with invalid UUID from renderer
277
+ // Keep the existing UUID in cache to prevent ensureFileExists() from regenerating it
278
+
279
+ // Return true to avoid breaking renderer expectations, but UUID is not actually changed
280
+ return true;
281
+ }
282
+
283
+ // Ensure cache is loaded
284
+ if (userDataCache === null) {
285
+ ensureFileExists();
286
+ }
287
+
288
+ // Validate cache structure
289
+ if (!userDataCache || typeof userDataCache !== 'object') {
290
+ console.error('set(): userDataCache is invalid, reinitializing');
291
+ userDataCache = { uuid: null, servers: [] };
292
+ }
293
+
294
+ // El uuid no se rescata acá: writeData() lo fija desde device-id en cada escritura.
295
+
296
+ // Handle case where nestedKey is an object but should be treated as value
297
+ // This happens when: set(key, object, undefined) or set(key, object, null)
298
+ if (nestedKey !== undefined && typeof nestedKey === 'object' &&
299
+ (value === undefined || value === null)) {
300
+ // Swap: value = nestedKey, nestedKey = undefined
301
+ value = nestedKey;
302
+ nestedKey = undefined;
303
+ }
304
+
305
+ // Also handle case where nestedKey is not a string (invalid for nested key)
306
+ if (nestedKey !== undefined && typeof nestedKey !== 'string') {
307
+ // If nestedKey is not a string, it can't be used as an object key
308
+ // Treat it as value instead
309
+ value = nestedKey;
310
+ nestedKey = undefined;
311
+ }
312
+
313
+ if (nestedKey !== undefined) {
314
+ // If nestedKey is provided, set nested value
315
+ // Check if current value is an object, if not, replace it with an object
316
+ if (!userDataCache[key] || typeof userDataCache[key] !== 'object' || Array.isArray(userDataCache[key])) {
317
+ userDataCache[key] = {};
318
+ }
319
+ userDataCache[key][nestedKey] = value;
320
+ } else {
321
+ // If only key is provided, set top-level value
322
+ userDataCache[key] = value;
323
+ }
324
+
325
+ return writeData(userDataCache);
326
+ }
327
+
328
+ function remove(key, nestedKey) {
329
+ // Handle both remove(key) and remove(key, nestedKey)
330
+ if (arguments.length === 1) {
331
+ nestedKey = undefined;
332
+ }
333
+
334
+ // Ensure cache is loaded
335
+ if (userDataCache === null) {
336
+ ensureFileExists();
337
+ }
338
+
339
+ if (nestedKey !== undefined) {
340
+ // If nestedKey is provided, remove nested value
341
+ if (userDataCache[key] && typeof userDataCache[key] === 'object' && userDataCache[key] !== null) {
342
+ delete userDataCache[key][nestedKey];
343
+ // If the object becomes empty, remove the entire key
344
+ if (Object.keys(userDataCache[key]).length === 0) {
345
+ delete userDataCache[key];
346
+ }
347
+ }
348
+ } else {
349
+ // If only key is provided, remove top-level value
350
+ delete userDataCache[key];
351
+ }
352
+
353
+ return writeData(userDataCache);
354
+ }
355
+
356
+ function getUUID() {
357
+ // Una sola fuente: device-id. Antes esto tenía su propia cadena de rescates que
358
+ // terminaba, como todas las demás, generando un uuid nuevo por su cuenta.
359
+ if (userDataCache === null) ensureFileExists();
360
+ return deviceUUID();
361
+ }
362
+
363
+ // Función específica para leer el lock desde el archivo (no del cache)
364
+ function getLockFromFile() {
365
+ try {
366
+ // Leer directamente del archivo, no del cache
367
+ if (!fs.existsSync(userDataPath)) {
368
+ return undefined;
369
+ }
370
+
371
+ const parsedData = JSON.parse(stripBom(fs.readFileSync(userDataPath, 'utf8')));
372
+
373
+ // Retornar el valor del lock si existe
374
+ return parsedData.lock;
375
+ } catch (err) {
376
+ console.error('Error reading lock from file:', err);
377
+ return undefined;
378
+ }
379
+ }
380
+
381
+ function getUserDataPath() {
382
+ return userDataDir;
383
+ }
384
+
385
+ function getUserDataDir() {
386
+ return userDataDir;
387
+ }
388
+
389
+ // Función auxiliar para validar formato UUID
390
+ function isValidUUID(uuid) {
391
+ if (!uuid || typeof uuid !== 'string') return false;
392
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
393
+ return uuidRegex.test(uuid);
394
+ }
395
+
396
+ // Función para validar y reparar datos de usuario
397
+ function validateAndRepairUserData() {
398
+ // Repara la CONFIGURACIÓN. El uuid no se toca acá: lo pone deviceUUID() y writeData
399
+ // lo vuelve a fijar en cada escritura. Antes esta función también intentaba
400
+ // "arreglar" el uuid y acababa inventando uno.
401
+ try {
402
+ const datos = ensureFileExists();
403
+ let reparar = false;
404
+
405
+ if (!Array.isArray(datos.servers)) {
406
+ console.warn('El campo servers no era un array; se repara.');
407
+ datos.servers = typeof datos.servers === 'string' ? [datos.servers] : [];
408
+ reparar = true;
409
+ }
410
+
411
+ if (reparar) writeData(datos);
412
+ return datos;
413
+ } catch (error) {
414
+ console.error('Error validando los datos de usuario:', error.message);
415
+ return ensureFileExists();
416
+ }
417
+ }
418
+
419
+ // NOT calling ensureFileExists() here anymore - will be called on demand
420
+ // This prevents double initialization and potential UUID regeneration
421
+
422
+ module.exports = {
423
+ ensureFileExists,
424
+ readData,
425
+ writeData,
426
+ get,
427
+ set,
428
+ remove,
429
+ getUUID,
430
+ getLockFromFile,
431
+ getUserDataPath,
432
+ getUserDataDir,
433
+ isValidUUID,
434
+ validateAndRepairUserData,
435
+ deviceUUID,
436
+ getDeviceIdPath: () => deviceIdPath,
437
+ };