openmates 0.27.0-alpha.1 → 0.27.0-alpha.11

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.
@@ -0,0 +1,1188 @@
1
+ // src/storage.ts
2
+ import {
3
+ chmodSync,
4
+ existsSync as existsSync2,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readFileSync as readFileSync2,
8
+ rmSync,
9
+ renameSync,
10
+ writeFileSync
11
+ } from "fs";
12
+ import lockfile from "proper-lockfile";
13
+ import { createHash as createHash2 } from "crypto";
14
+ import { homedir as homedir2 } from "os";
15
+ import { isAbsolute, join, resolve } from "path";
16
+
17
+ // src/keychain.ts
18
+ import { execFileSync } from "child_process";
19
+ import {
20
+ createHash,
21
+ pbkdf2Sync,
22
+ createDecipheriv
23
+ } from "crypto";
24
+ import { existsSync, readFileSync } from "fs";
25
+ import { homedir, platform, userInfo } from "os";
26
+ var KEYCHAIN_SERVICE = "OpenMates";
27
+ var AES_GCM_IV_LENGTH = 12;
28
+ var PBKDF2_ITERATIONS = 1e5;
29
+ var KEYCHAIN_TIMEOUT_MS = 5e3;
30
+ function storeMasterKey(key, hashedEmail, existingStorage) {
31
+ try {
32
+ keychainStore(key, hashedEmail);
33
+ return { type: "keychain" };
34
+ } catch (error) {
35
+ if (existingStorage === "keychain") {
36
+ throw new Error("Existing OS keyring entry is unavailable; local credential storage was preserved.", { cause: error });
37
+ }
38
+ }
39
+ return { type: "file" };
40
+ }
41
+ function retrieveMasterKey(type, hashedEmail, encryptedData) {
42
+ switch (type) {
43
+ case "keychain":
44
+ try {
45
+ return keychainRetrieve(hashedEmail);
46
+ } catch {
47
+ return null;
48
+ }
49
+ case "encrypted":
50
+ if (!encryptedData) return null;
51
+ try {
52
+ return decryptWithMachineKey(encryptedData);
53
+ } catch {
54
+ return null;
55
+ }
56
+ case "plaintext":
57
+ case "file":
58
+ return null;
59
+ default:
60
+ return null;
61
+ }
62
+ }
63
+ function deleteMasterKeyStrict(type, hashedEmail) {
64
+ if (type !== "keychain") return;
65
+ try {
66
+ keychainDelete(hashedEmail);
67
+ } catch (error) {
68
+ throw new Error("OS keyring cleanup failed; a stored key may remain.", { cause: error });
69
+ }
70
+ }
71
+ function keychainStore(key, account) {
72
+ const os = platform();
73
+ if (os === "darwin") {
74
+ execFileSync("security", [
75
+ "add-generic-password",
76
+ "-s",
77
+ KEYCHAIN_SERVICE,
78
+ "-a",
79
+ account,
80
+ "-w",
81
+ key,
82
+ "-U"
83
+ ], { timeout: KEYCHAIN_TIMEOUT_MS, stdio: "pipe" });
84
+ return;
85
+ }
86
+ if (os === "linux") {
87
+ execFileSync("secret-tool", [
88
+ "store",
89
+ "--label",
90
+ KEYCHAIN_SERVICE,
91
+ "service",
92
+ KEYCHAIN_SERVICE,
93
+ "account",
94
+ account
95
+ ], {
96
+ timeout: KEYCHAIN_TIMEOUT_MS,
97
+ stdio: ["pipe", "pipe", "pipe"],
98
+ input: key
99
+ });
100
+ return;
101
+ }
102
+ throw new Error(`Keychain not supported on ${os}`);
103
+ }
104
+ function keychainRetrieve(account) {
105
+ const os = platform();
106
+ if (os === "darwin") {
107
+ const result = execFileSync("security", [
108
+ "find-generic-password",
109
+ "-s",
110
+ KEYCHAIN_SERVICE,
111
+ "-a",
112
+ account,
113
+ "-w"
114
+ ], { timeout: KEYCHAIN_TIMEOUT_MS, stdio: "pipe", encoding: "utf-8" });
115
+ const value = result.trim();
116
+ return value || null;
117
+ }
118
+ if (os === "linux") {
119
+ const result = execFileSync("secret-tool", [
120
+ "lookup",
121
+ "service",
122
+ KEYCHAIN_SERVICE,
123
+ "account",
124
+ account
125
+ ], { timeout: KEYCHAIN_TIMEOUT_MS, stdio: "pipe", encoding: "utf-8" });
126
+ const value = result.trim();
127
+ return value || null;
128
+ }
129
+ throw new Error(`Keychain not supported on ${os}`);
130
+ }
131
+ function keychainDelete(account) {
132
+ const os = platform();
133
+ if (os === "darwin") {
134
+ execFileSync("security", [
135
+ "delete-generic-password",
136
+ "-s",
137
+ KEYCHAIN_SERVICE,
138
+ "-a",
139
+ account
140
+ ], { timeout: KEYCHAIN_TIMEOUT_MS, stdio: "pipe" });
141
+ return;
142
+ }
143
+ if (os === "linux") {
144
+ execFileSync("secret-tool", [
145
+ "clear",
146
+ "service",
147
+ KEYCHAIN_SERVICE,
148
+ "account",
149
+ account
150
+ ], { timeout: KEYCHAIN_TIMEOUT_MS, stdio: "pipe" });
151
+ return;
152
+ }
153
+ throw new Error(`Keychain not supported on ${os}`);
154
+ }
155
+ function deriveMachineKey() {
156
+ const entropy = getMachineEntropy();
157
+ const username = userInfo().username;
158
+ const salt = `OpenMates-machine-key-${username}`;
159
+ return pbkdf2Sync(entropy, salt, PBKDF2_ITERATIONS, 32, "sha256");
160
+ }
161
+ function getMachineEntropy() {
162
+ const os = platform();
163
+ if (os === "darwin") {
164
+ try {
165
+ const output = execFileSync("ioreg", [
166
+ "-rd1",
167
+ "-c",
168
+ "IOPlatformExpertDevice"
169
+ ], { timeout: KEYCHAIN_TIMEOUT_MS, encoding: "utf-8", stdio: "pipe" });
170
+ const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
171
+ if (match?.[1]) return match[1];
172
+ } catch {
173
+ }
174
+ }
175
+ if (os === "linux") {
176
+ try {
177
+ const machineIdPath = "/etc/machine-id";
178
+ if (existsSync(machineIdPath)) {
179
+ const id = readFileSync(machineIdPath, "utf-8").trim();
180
+ if (id) return id;
181
+ }
182
+ } catch {
183
+ }
184
+ }
185
+ return createHash("sha256").update(`${homedir()}-${userInfo().username}-${os}`).digest("hex");
186
+ }
187
+ function decryptWithMachineKey(encryptedB64) {
188
+ const key = deriveMachineKey();
189
+ const combined = Buffer.from(encryptedB64, "base64");
190
+ if (combined.length <= AES_GCM_IV_LENGTH + 16) {
191
+ throw new Error("Encrypted data too short");
192
+ }
193
+ const iv = combined.subarray(0, AES_GCM_IV_LENGTH);
194
+ const authTag = combined.subarray(combined.length - 16);
195
+ const ciphertext = combined.subarray(AES_GCM_IV_LENGTH, combined.length - 16);
196
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
197
+ decipher.setAuthTag(authTag);
198
+ const decrypted = Buffer.concat([
199
+ decipher.update(ciphertext),
200
+ decipher.final()
201
+ ]);
202
+ return decrypted.toString("utf-8");
203
+ }
204
+
205
+ // src/storage.ts
206
+ var PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
207
+ var TRUSTED_ACCOUNT_FILE = "trusted_account.json";
208
+ function resolveStateDir(options = {}) {
209
+ const homeDir = options.homeDir ?? homedir2();
210
+ const stateDir = options.stateDir ?? process.env.OPENMATES_STATE_DIR?.trim();
211
+ if (stateDir) {
212
+ if (!isAbsolute(stateDir)) throw new Error("OPENMATES_STATE_DIR must be an absolute path.");
213
+ return resolve(stateDir);
214
+ }
215
+ const profile = options.profile ?? process.env.OPENMATES_PROFILE?.trim();
216
+ if (!profile) return join(homeDir, ".openmates");
217
+ if (!PROFILE_NAME_PATTERN.test(profile)) {
218
+ throw new Error("OPENMATES_PROFILE must contain only lowercase letters, numbers, and hyphens.");
219
+ }
220
+ return join(homeDir, ".openmates", "profiles", profile);
221
+ }
222
+ function getStateDir() {
223
+ return resolveStateDir();
224
+ }
225
+ function resolveKeyStorageId(storageId, profile = process.env.OPENMATES_PROFILE?.trim()) {
226
+ if (!profile) return storageId;
227
+ if (!PROFILE_NAME_PATTERN.test(profile)) {
228
+ throw new Error("OPENMATES_PROFILE must contain only lowercase letters, numbers, and hyphens.");
229
+ }
230
+ return `profile:${profile}:${storageId}`;
231
+ }
232
+ function ensureStateDir() {
233
+ const dir = getStateDir();
234
+ if (!existsSync2(dir)) {
235
+ mkdirSync(dir, { recursive: true, mode: 448 });
236
+ }
237
+ chmodSync(dir, 448);
238
+ return dir;
239
+ }
240
+ function readJsonFile(filePath) {
241
+ if (!existsSync2(filePath)) {
242
+ return null;
243
+ }
244
+ try {
245
+ return JSON.parse(readFileSync2(filePath, "utf-8"));
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ function writeJsonFile(filePath, data) {
251
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
252
+ `, {
253
+ mode: 384
254
+ });
255
+ chmodSync(filePath, 384);
256
+ }
257
+ function loadAnonymousId() {
258
+ const filePath = join(getStateDir(), "anonymous.json");
259
+ const data = readJsonFile(filePath);
260
+ return typeof data?.anonymousId === "string" && data.anonymousId.length > 0 ? data.anonymousId : null;
261
+ }
262
+ function saveAnonymousId(anonymousId) {
263
+ const filePath = join(ensureStateDir(), "anonymous.json");
264
+ writeJsonFile(filePath, {
265
+ anonymousId,
266
+ createdAt: Math.floor(Date.now() / 1e3)
267
+ });
268
+ }
269
+ var SESSION_LOCK_STALE_MS = 3e4;
270
+ var SESSION_LOCK_POLL_MS = 50;
271
+ async function withSessionRefreshLock(operation) {
272
+ const path = join(ensureStateDir(), "session.json");
273
+ const release = await lockfile.lock(`${path}.refresh`, {
274
+ realpath: false,
275
+ stale: SESSION_LOCK_STALE_MS,
276
+ retries: { retries: 600, factor: 1, minTimeout: SESSION_LOCK_POLL_MS, maxTimeout: SESSION_LOCK_POLL_MS }
277
+ });
278
+ try {
279
+ return await operation();
280
+ } finally {
281
+ await release();
282
+ }
283
+ }
284
+ function saveSession(session, options = {}) {
285
+ const path = join(ensureStateDir(), "session.json");
286
+ let release;
287
+ const waitCell = new Int32Array(new SharedArrayBuffer(4));
288
+ for (let attempt = 0; ; attempt++) {
289
+ try {
290
+ release = lockfile.lockSync(`${path}.write`, { realpath: false, stale: SESSION_LOCK_STALE_MS });
291
+ break;
292
+ } catch (error) {
293
+ if (error.code !== "ELOCKED" || attempt >= 100) throw error;
294
+ Atomics.wait(waitCell, 0, 0, SESSION_LOCK_POLL_MS);
295
+ }
296
+ }
297
+ try {
298
+ const current = readJsonFile(path);
299
+ if (current && !options.replace) {
300
+ if (current.sessionId !== session.sessionId || current.hashedEmail !== session.hashedEmail || current.apiUrl !== session.apiUrl) {
301
+ throw new Error("Login session changed in another process. Retry the command using the current profile.");
302
+ }
303
+ if (options.expectedRefreshToken !== void 0) {
304
+ if (current.cookies.auth_refresh_token !== options.expectedRefreshToken) {
305
+ throw new Error("Login credentials changed during refresh. Retry with the current profile.");
306
+ }
307
+ } else {
308
+ session.cookies = { ...current.cookies };
309
+ session.wsToken = current.wsToken;
310
+ }
311
+ if (typeof current.pairedSessionExpiresAt === "number") {
312
+ session.pairedSessionExpiresAt = Math.min(
313
+ current.pairedSessionExpiresAt,
314
+ session.pairedSessionExpiresAt ?? current.pairedSessionExpiresAt
315
+ );
316
+ }
317
+ }
318
+ writeSession(session, current?.hashedEmail === session.hashedEmail ? current : null);
319
+ } finally {
320
+ release();
321
+ }
322
+ }
323
+ function writeSession(session, previous) {
324
+ const filePath = join(ensureStateDir(), "session.json");
325
+ const result = storeMasterKey(
326
+ session.masterKeyExportedB64,
327
+ resolveKeyStorageId(session.hashedEmail),
328
+ previous?.masterKeyStorage
329
+ );
330
+ const onDisk = {
331
+ apiUrl: session.apiUrl,
332
+ sessionId: session.sessionId,
333
+ wsToken: session.wsToken,
334
+ cookies: session.cookies,
335
+ hashedEmail: session.hashedEmail,
336
+ userEmailSalt: session.userEmailSalt,
337
+ createdAt: session.createdAt,
338
+ authorizerDeviceName: session.authorizerDeviceName,
339
+ autoLogoutMinutes: session.autoLogoutMinutes,
340
+ pairedSessionExpiresAt: session.pairedSessionExpiresAt ?? null,
341
+ activeTeamId: session.activeTeamId ?? null,
342
+ masterKeyStorage: result.type
343
+ };
344
+ if (result.type === "encrypted") {
345
+ onDisk.masterKeyEncrypted = result.encryptedData;
346
+ } else if (result.type === "file" || result.type === "plaintext") {
347
+ onDisk.masterKeyExportedB64 = session.masterKeyExportedB64;
348
+ }
349
+ if (session.emailEncryptionKeyB64) {
350
+ const emailKeyResult = storeMasterKey(
351
+ session.emailEncryptionKeyB64,
352
+ resolveKeyStorageId(`${session.hashedEmail}:email`),
353
+ previous?.emailEncryptionKeyStorage
354
+ );
355
+ onDisk.emailEncryptionKeyStorage = emailKeyResult.type;
356
+ if (emailKeyResult.type === "encrypted") {
357
+ onDisk.emailEncryptionKeyEncrypted = emailKeyResult.encryptedData;
358
+ } else if (emailKeyResult.type === "file" || emailKeyResult.type === "plaintext") {
359
+ onDisk.emailEncryptionKeyB64 = session.emailEncryptionKeyB64;
360
+ }
361
+ }
362
+ const temporary = `${filePath}.${process.pid}.tmp`;
363
+ try {
364
+ writeJsonFile(temporary, onDisk);
365
+ renameSync(temporary, filePath);
366
+ } finally {
367
+ rmSync(temporary, { force: true });
368
+ }
369
+ }
370
+ function loadSession() {
371
+ const filePath = join(ensureStateDir(), "session.json");
372
+ const onDisk = readJsonFile(filePath);
373
+ if (!onDisk) return null;
374
+ if (typeof onDisk.pairedSessionExpiresAt === "number" && onDisk.pairedSessionExpiresAt <= Date.now()) {
375
+ purgeLocalPrivateData();
376
+ return null;
377
+ }
378
+ let masterKey = null;
379
+ if (!onDisk.masterKeyStorage) {
380
+ masterKey = onDisk.masterKeyExportedB64 ?? null;
381
+ return masterKey ? buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk)) : null;
382
+ }
383
+ switch (onDisk.masterKeyStorage) {
384
+ case "keychain":
385
+ masterKey = retrieveMasterKey("keychain", resolveKeyStorageId(onDisk.hashedEmail));
386
+ if (!masterKey) throw new Error("Existing OS keyring entry is unavailable; local session was preserved.");
387
+ break;
388
+ case "encrypted":
389
+ masterKey = retrieveMasterKey(
390
+ "encrypted",
391
+ resolveKeyStorageId(onDisk.hashedEmail),
392
+ onDisk.masterKeyEncrypted
393
+ );
394
+ break;
395
+ case "plaintext":
396
+ case "file":
397
+ masterKey = onDisk.masterKeyExportedB64 ?? null;
398
+ break;
399
+ }
400
+ if (!masterKey) {
401
+ process.stderr.write(
402
+ `Failed to retrieve master key \u2014 session invalid
403
+ `
404
+ );
405
+ return null;
406
+ }
407
+ return buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk));
408
+ }
409
+ function getCredentialStorageMode() {
410
+ const onDisk = readJsonFile(join(getStateDir(), "session.json"));
411
+ if (!onDisk) return "none";
412
+ if (onDisk.masterKeyStorage === "keychain") return "os-keyring";
413
+ if (onDisk.masterKeyStorage === "encrypted") return "legacy-machine-id";
414
+ return "owner-only-file";
415
+ }
416
+ function clearSession() {
417
+ const filePath = join(ensureStateDir(), "session.json");
418
+ const onDisk = readJsonFile(filePath);
419
+ let cleanupError = null;
420
+ if (onDisk?.masterKeyStorage) {
421
+ try {
422
+ deleteMasterKeyStrict(onDisk.masterKeyStorage, resolveKeyStorageId(onDisk.hashedEmail));
423
+ } catch (error) {
424
+ cleanupError = error;
425
+ }
426
+ }
427
+ if (onDisk?.emailEncryptionKeyStorage) {
428
+ try {
429
+ deleteMasterKeyStrict(onDisk.emailEncryptionKeyStorage, resolveKeyStorageId(`${onDisk.hashedEmail}:email`));
430
+ } catch (error) {
431
+ cleanupError ??= error;
432
+ }
433
+ }
434
+ if (onDisk?.activeTeamId) {
435
+ deleteLocalTeamKey(onDisk.hashedEmail, onDisk.activeTeamId);
436
+ }
437
+ if (existsSync2(filePath)) {
438
+ rmSync(filePath);
439
+ }
440
+ if (cleanupError) throw cleanupError;
441
+ }
442
+ function purgeLocalPrivateData() {
443
+ const stateDir = ensureStateDir();
444
+ const sessionFilePath = join(stateDir, "session.json");
445
+ const onDisk = readJsonFile(sessionFilePath);
446
+ const hashedEmail = onDisk?.hashedEmail ?? null;
447
+ let cleanupError = null;
448
+ try {
449
+ clearSession();
450
+ } catch (error) {
451
+ cleanupError = error;
452
+ }
453
+ try {
454
+ purgeLocalTeamKeys(hashedEmail);
455
+ } catch (error) {
456
+ cleanupError ??= error;
457
+ }
458
+ try {
459
+ purgeSyncCaches(stateDir);
460
+ } catch (error) {
461
+ cleanupError ??= error;
462
+ }
463
+ if (cleanupError) throw cleanupError;
464
+ }
465
+ function purgeLocalTeamKeys(hashedEmail) {
466
+ const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
467
+ const keys = readJsonFile(filePath);
468
+ if (!keys) return;
469
+ let changed = false;
470
+ const prefix = hashedEmail ? resolveKeyStorageId(`${hashedEmail}:team:`) : null;
471
+ for (const [storageId, entry] of Object.entries(keys.teams)) {
472
+ if (prefix && !storageId.startsWith(prefix)) continue;
473
+ deleteMasterKeyStrict(entry.storage, storageId);
474
+ delete keys.teams[storageId];
475
+ changed = true;
476
+ }
477
+ if (!changed) return;
478
+ if (Object.keys(keys.teams).length === 0) {
479
+ rmSync(filePath, { force: true });
480
+ return;
481
+ }
482
+ writeJsonFile(filePath, keys);
483
+ }
484
+ function purgeSyncCaches(stateDir) {
485
+ rmSync(join(stateDir, "project_file_privacy"), { recursive: true, force: true });
486
+ for (const fileName of readdirSync(stateDir)) {
487
+ if (fileName === SYNC_CACHE_FILE || fileName.startsWith("sync_cache.team.") && fileName.endsWith(".json")) {
488
+ rmSync(join(stateDir, fileName), { force: true });
489
+ }
490
+ }
491
+ }
492
+ function getEmailEncryptionKeyFromDisk(onDisk) {
493
+ if (!onDisk.emailEncryptionKeyStorage) return onDisk.emailEncryptionKeyB64 ?? null;
494
+ switch (onDisk.emailEncryptionKeyStorage) {
495
+ case "keychain": {
496
+ const key = retrieveMasterKey("keychain", resolveKeyStorageId(`${onDisk.hashedEmail}:email`));
497
+ if (!key) throw new Error("Existing email keyring entry is unavailable; local session was preserved.");
498
+ return key;
499
+ }
500
+ case "encrypted":
501
+ return retrieveMasterKey(
502
+ "encrypted",
503
+ resolveKeyStorageId(`${onDisk.hashedEmail}:email`),
504
+ onDisk.emailEncryptionKeyEncrypted
505
+ );
506
+ case "plaintext":
507
+ case "file":
508
+ return onDisk.emailEncryptionKeyB64 ?? null;
509
+ }
510
+ }
511
+ function buildSession(onDisk, masterKey, emailEncryptionKey) {
512
+ return {
513
+ apiUrl: onDisk.apiUrl,
514
+ sessionId: onDisk.sessionId,
515
+ wsToken: onDisk.wsToken,
516
+ cookies: onDisk.cookies,
517
+ masterKeyExportedB64: masterKey,
518
+ emailEncryptionKeyB64: emailEncryptionKey,
519
+ hashedEmail: onDisk.hashedEmail,
520
+ userEmailSalt: onDisk.userEmailSalt,
521
+ createdAt: onDisk.createdAt,
522
+ authorizerDeviceName: onDisk.authorizerDeviceName,
523
+ autoLogoutMinutes: onDisk.autoLogoutMinutes,
524
+ pairedSessionExpiresAt: onDisk.pairedSessionExpiresAt ?? null,
525
+ activeTeamId: onDisk.activeTeamId ?? null
526
+ };
527
+ }
528
+ var SYNC_CACHE_FILE = "sync_cache.json";
529
+ var LOCAL_TEAM_KEYS_FILE = "team_keys.json";
530
+ function teamKeyStorageId(hashedEmail, teamId) {
531
+ const digest = createHash2("sha256").update(teamId).digest("hex").slice(0, 32);
532
+ return resolveKeyStorageId(`${hashedEmail}:team:${digest}`);
533
+ }
534
+ function saveLocalTeamKey(hashedEmail, teamId, teamKeyB64) {
535
+ const storageId = teamKeyStorageId(hashedEmail, teamId);
536
+ const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
537
+ const keys = readJsonFile(filePath) ?? { teams: {} };
538
+ const result = storeMasterKey(teamKeyB64, storageId, keys.teams[storageId]?.storage);
539
+ keys.teams[storageId] = {
540
+ storage: result.type,
541
+ ...result.type === "encrypted" ? { encryptedData: result.encryptedData } : {},
542
+ ...result.type === "file" || result.type === "plaintext" ? { plaintextKeyB64: teamKeyB64 } : {}
543
+ };
544
+ writeJsonFile(filePath, keys);
545
+ }
546
+ function loadLocalTeamKey(hashedEmail, teamId) {
547
+ const storageId = teamKeyStorageId(hashedEmail, teamId);
548
+ const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
549
+ const entry = readJsonFile(filePath)?.teams[storageId];
550
+ if (!entry) return null;
551
+ if (entry.storage === "plaintext" || entry.storage === "file") return entry.plaintextKeyB64 ?? null;
552
+ const key = retrieveMasterKey(entry.storage, storageId, entry.encryptedData);
553
+ if (entry.storage === "keychain" && !key) throw new Error("Existing team keyring entry is unavailable; local team key was preserved.");
554
+ return key;
555
+ }
556
+ function deleteLocalTeamKey(hashedEmail, teamId) {
557
+ const storageId = teamKeyStorageId(hashedEmail, teamId);
558
+ const filePath = join(ensureStateDir(), LOCAL_TEAM_KEYS_FILE);
559
+ const keys = readJsonFile(filePath);
560
+ if (keys?.teams[storageId]) {
561
+ deleteMasterKeyStrict(keys.teams[storageId].storage, storageId);
562
+ delete keys.teams[storageId];
563
+ writeJsonFile(filePath, keys);
564
+ }
565
+ }
566
+ function pruneLocalTeamArtifacts(hashedEmail, teamIds) {
567
+ const stateDir = ensureStateDir();
568
+ const allowedKeyIds = new Set(teamIds.map((teamId) => teamKeyStorageId(hashedEmail, teamId)));
569
+ const keysFilePath = join(stateDir, LOCAL_TEAM_KEYS_FILE);
570
+ const keys = readJsonFile(keysFilePath);
571
+ if (keys) {
572
+ let changed = false;
573
+ const prefix = resolveKeyStorageId(`${hashedEmail}:team:`);
574
+ for (const storageId of Object.keys(keys.teams)) {
575
+ if (storageId.startsWith(prefix) && !allowedKeyIds.has(storageId)) {
576
+ deleteMasterKeyStrict(keys.teams[storageId].storage, storageId);
577
+ delete keys.teams[storageId];
578
+ changed = true;
579
+ }
580
+ }
581
+ if (changed) writeJsonFile(keysFilePath, keys);
582
+ }
583
+ const allowedCacheFiles = new Set(teamIds.map((teamId) => syncCacheFile(teamId)));
584
+ for (const fileName of readdirSync(stateDir)) {
585
+ if (fileName.startsWith("sync_cache.team.") && fileName.endsWith(".json") && !allowedCacheFiles.has(fileName)) {
586
+ rmSync(join(stateDir, fileName), { force: true });
587
+ }
588
+ }
589
+ }
590
+ function syncCacheFile(teamId) {
591
+ if (!teamId) return SYNC_CACHE_FILE;
592
+ const digest = createHash2("sha256").update(teamId).digest("hex").slice(0, 32);
593
+ return `sync_cache.team.${digest}.json`;
594
+ }
595
+ function saveSyncCache(cache, teamId) {
596
+ const filePath = join(ensureStateDir(), syncCacheFile(teamId));
597
+ writeJsonFile(filePath, cache);
598
+ }
599
+ function loadSyncCache(teamId) {
600
+ const filePath = join(ensureStateDir(), syncCacheFile(teamId));
601
+ return readJsonFile(filePath);
602
+ }
603
+ function clearSyncCache(teamId) {
604
+ const filePath = join(ensureStateDir(), syncCacheFile(teamId));
605
+ if (existsSync2(filePath)) {
606
+ rmSync(filePath);
607
+ }
608
+ }
609
+ function isSyncCacheFresh(maxAgeMs = 3e5, teamId) {
610
+ const cache = loadSyncCache(teamId);
611
+ if (!cache) return false;
612
+ return Date.now() - cache.syncedAt < maxAgeMs;
613
+ }
614
+ function loadTrustedAccountId() {
615
+ const trusted = readJsonFile(join(getStateDir(), TRUSTED_ACCOUNT_FILE));
616
+ return typeof trusted?.accountId === "string" && trusted.accountId.trim() ? trusted.accountId.trim() : null;
617
+ }
618
+ function saveTrustedAccountId(accountId) {
619
+ const normalized = accountId.trim();
620
+ if (!normalized) throw new Error("Cannot trust an empty OpenMates account ID.");
621
+ const existing = loadTrustedAccountId();
622
+ if (existing && existing !== normalized) {
623
+ throw new Error("OpenMates account mismatch: this CLI profile is already trusted for another account.");
624
+ }
625
+ writeJsonFile(join(ensureStateDir(), TRUSTED_ACCOUNT_FILE), { accountId: normalized });
626
+ }
627
+ function assertTrustedAccountId(expectedAccountId, actualAccountId) {
628
+ if (!expectedAccountId) {
629
+ throw new Error("This CLI profile is not trusted yet. Run `openmates login` and approve the intended account.");
630
+ }
631
+ if (expectedAccountId !== actualAccountId) {
632
+ throw new Error("OpenMates account mismatch: refusing authenticated access from this CLI profile.");
633
+ }
634
+ }
635
+
636
+ // src/privacyWorker.ts
637
+ import { createHash as createHash4 } from "crypto";
638
+ import { spawn } from "child_process";
639
+ import { chmod as chmod2, lstat as lstat2, readFile as readFile2, rm as rm2 } from "fs/promises";
640
+ import { availableParallelism as availableParallelism2, freemem } from "os";
641
+ import { createConnection, createServer } from "net";
642
+ import { fileURLToPath as fileURLToPath2 } from "url";
643
+ import lockfile3 from "proper-lockfile";
644
+
645
+ // src/privacyModel.ts
646
+ import { createHash as createHash3, randomUUID } from "crypto";
647
+ import { createReadStream } from "fs";
648
+ import { chmod, copyFile, lstat, mkdir, open, readFile, rename, rm, statfs, writeFile } from "fs/promises";
649
+ import { availableParallelism, homedir as homedir3, totalmem } from "os";
650
+ import { dirname, join as join2, resolve as resolve2 } from "path";
651
+ import { fileURLToPath } from "url";
652
+ import lockfile2 from "proper-lockfile";
653
+ var ENHANCED_ANONYMIZATION_LABEL = "Enhanced personal data anonymization (offline AI model)";
654
+ var PRIVACY_MODEL = {
655
+ revision: "935d86882f4b5dc6e0eef05aae48ae5ba0a82a7a",
656
+ bytes: 1637572192,
657
+ sha256: "80efc1803eda7c095a79741d2008c07e2e0a57b01bac8825fbeb448fd097998c",
658
+ url: "https://huggingface.co/LocalAI-io/privacy-filter-GGUF/resolve/935d86882f4b5dc6e0eef05aae48ae5ba0a82a7a/privacy-filter-q8.gguf"
659
+ };
660
+ var defaults = () => ({ version: 1, enabled: false, documents: false, projects: [], offerSeen: false, idleSeconds: 120 });
661
+ function privacyRoot() {
662
+ return resolve2(process.env.OPENMATES_PRIVACY_DIR?.trim() || join2(homedir3(), ".openmates", "privacy-model"));
663
+ }
664
+ function privacyModelPath() {
665
+ return join2(privacyRoot(), PRIVACY_MODEL.revision, "model.gguf");
666
+ }
667
+ function privacySocketPath() {
668
+ return join2(privacyRoot(), "worker.sock");
669
+ }
670
+ function privacyProfile() {
671
+ return createHash3("sha256").update(resolveStateDir()).digest("hex");
672
+ }
673
+ function privacyRuntimeDirectory() {
674
+ return fileURLToPath(new URL("../fixtures/privacy-runtime/linux-arm64/", import.meta.url));
675
+ }
676
+ function privacyError(code) {
677
+ return Object.assign(new Error(`${ENHANCED_ANONYMIZATION_LABEL}: ${code}. No enhanced-mode text was sent. Retry, or explicitly disable this mode with 'openmates privacy disable'.`), { code });
678
+ }
679
+ async function privateDirectory(path) {
680
+ await mkdir(path, { recursive: true, mode: 448 });
681
+ const s = await lstat(path);
682
+ if (!s.isDirectory() || s.isSymbolicLink() || process.getuid && s.uid !== process.getuid()) throw privacyError("privacy_unsafe_directory");
683
+ await chmod(path, 448);
684
+ }
685
+ async function readPrivateJson(path) {
686
+ try {
687
+ const s = await lstat(path);
688
+ if (!s.isFile() || s.isSymbolicLink() || s.mode & 63 || process.getuid && s.uid !== process.getuid()) throw privacyError("privacy_unsafe_state");
689
+ return JSON.parse(await readFile(path, "utf8"));
690
+ } catch (error) {
691
+ if (error.code === "ENOENT") return null;
692
+ throw error;
693
+ }
694
+ }
695
+ async function writePrivateJson(path, value) {
696
+ await privateDirectory(dirname(path));
697
+ const temporary = path + "." + randomUUID();
698
+ try {
699
+ await writeFile(temporary, JSON.stringify(value) + "\n", { flag: "wx", mode: 384 });
700
+ await rename(temporary, path);
701
+ } finally {
702
+ await rm(temporary, { force: true });
703
+ }
704
+ }
705
+ async function readPrivacyPreferences() {
706
+ const data = await readPrivateJson(join2(resolveStateDir(), "privacy-preferences.json"));
707
+ if (!data) return defaults();
708
+ if (data.version !== 1 || typeof data.enabled !== "boolean" || typeof data.documents !== "boolean" || !Array.isArray(data.projects) || data.projects.length > 512 || data.projects.some((id) => typeof id !== "string" || !/^[0-9a-f-]{36}$/i.test(id))) throw privacyError("privacy_invalid_preferences");
709
+ return { ...defaults(), ...data, projects: [...data.projects], idleSeconds: Math.max(15, Math.min(3600, Number(data.idleSeconds) || 120)) };
710
+ }
711
+ async function updatePrivacyPreferences(change) {
712
+ const directory = resolveStateDir();
713
+ await privateDirectory(directory);
714
+ const release = await lockfile2.lock(directory, { lockfilePath: join2(directory, "privacy-preferences.lock"), retries: { retries: 10, minTimeout: 50, maxTimeout: 100 }, stale: 3e4 });
715
+ try {
716
+ const value = change(await readPrivacyPreferences());
717
+ await writePrivateJson(join2(directory, "privacy-preferences.json"), value);
718
+ return value;
719
+ } finally {
720
+ await release();
721
+ }
722
+ }
723
+ function enhancedScopeEnabled(preferences, scope) {
724
+ return preferences.enabled && (scope.kind === "message" || scope.kind === "document" && preferences.documents || scope.kind === "project" && !!scope.projectId && preferences.projects.includes(scope.projectId));
725
+ }
726
+ async function privacyCapability() {
727
+ if (process.platform !== "linux" || process.arch !== "arm64") return { supported: false, reason: "The first packaged runtime supports Linux ARM64." };
728
+ const features = await readFile("/proc/cpuinfo", "utf8").catch(() => "");
729
+ if (!/\basimddp\b/.test(features) || !/\basimdhp\b/.test(features)) return { supported: false, reason: "CPU FP16 and dot-product instructions are required." };
730
+ if (totalmem() < 4 * 2 ** 30 || availableParallelism() < 2) return { supported: false, reason: "At least two CPU cores and 4 GB RAM are required; 8 GB is recommended for development." };
731
+ try {
732
+ await lstat(join2(privacyRuntimeDirectory(), "manifest.json"));
733
+ } catch {
734
+ return { supported: false, reason: "The packaged native runtime is unavailable in this CLI build." };
735
+ }
736
+ return { supported: true };
737
+ }
738
+ async function fileDigest(path) {
739
+ const hash = createHash3("sha256");
740
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
741
+ return hash.digest("hex");
742
+ }
743
+ async function verifyInstalledModel() {
744
+ const path = privacyModelPath();
745
+ const s = await lstat(path);
746
+ if (!s.isFile() || s.isSymbolicLink() || s.mode & 63 || s.size !== PRIVACY_MODEL.bytes || process.getuid && s.uid !== process.getuid() || await fileDigest(path) !== PRIVACY_MODEL.sha256) throw privacyError("privacy_model_integrity_failed");
747
+ }
748
+ async function verifyPrivacyRuntime() {
749
+ const directory = privacyRuntimeDirectory();
750
+ const manifest = JSON.parse(await readFile(join2(directory, "manifest.json"), "utf8"));
751
+ if (manifest.revision !== "735a6c28607ee82afc3a670383f41b55266a3b9a" || manifest.protocol !== 1 || !manifest.files?.length) throw privacyError("privacy_runtime_integrity_failed");
752
+ for (const item of manifest.files) {
753
+ if (!/^[A-Za-z0-9._-]+$/.test(item.name)) throw privacyError("privacy_runtime_integrity_failed");
754
+ const path = join2(directory, item.name);
755
+ const s = await lstat(path);
756
+ if (!s.isFile() || s.isSymbolicLink() || s.size !== item.bytes || await fileDigest(path) !== item.sha256) throw privacyError("privacy_runtime_integrity_failed");
757
+ }
758
+ return join2(directory, "openmates-pii-worker");
759
+ }
760
+ async function privacyStatus() {
761
+ const [preferences, capability] = await Promise.all([readPrivacyPreferences(), privacyCapability()]);
762
+ let installed = false;
763
+ try {
764
+ const s = await lstat(privacyModelPath());
765
+ installed = s.isFile() && !s.isSymbolicLink() && s.size === PRIVACY_MODEL.bytes;
766
+ } catch (error) {
767
+ if (error.code !== "ENOENT") throw error;
768
+ }
769
+ return {
770
+ feature: ENHANCED_ANONYMIZATION_LABEL,
771
+ state: !capability.supported ? "unsupported" : !installed ? "not_installed" : preferences.enabled ? "installed_enabled" : "installed_disabled",
772
+ reason: capability.reason,
773
+ model_bytes: PRIVACY_MODEL.bytes,
774
+ memory_gib: 2,
775
+ local_only: true,
776
+ messages: preferences.enabled,
777
+ documents: preferences.enabled && preferences.documents,
778
+ projects: preferences.projects,
779
+ installed,
780
+ idle_seconds: preferences.idleSeconds
781
+ };
782
+ }
783
+ async function claimPrivacyOffer() {
784
+ if (!(await privacyCapability()).supported) return false;
785
+ await privateDirectory(privacyRoot());
786
+ const path = join2(privacyRoot(), "tui-offer-seen.json");
787
+ try {
788
+ const file = await open(path, "wx", 384);
789
+ try {
790
+ await file.writeFile(JSON.stringify({ seen: true }) + "\n");
791
+ } finally {
792
+ await file.close();
793
+ }
794
+ return !(await privacyStatus()).installed;
795
+ } catch (error) {
796
+ if (error.code === "EEXIST") return false;
797
+ throw error;
798
+ }
799
+ }
800
+ async function installPrivacyModel(options = {}) {
801
+ const capability = await privacyCapability();
802
+ if (!capability.supported) throw new Error(capability.reason);
803
+ await verifyPrivacyRuntime();
804
+ await privateDirectory(privacyRoot());
805
+ const release = await lockfile2.lock(privacyRoot(), { lockfilePath: join2(privacyRoot(), "install.lock"), stale: 6e4, update: 1e4 });
806
+ try {
807
+ const directory = dirname(privacyModelPath());
808
+ await privateDirectory(directory);
809
+ try {
810
+ await verifyInstalledModel();
811
+ if (!options.downloadOnly) await updatePrivacyPreferences((p) => ({ ...p, enabled: true, offerSeen: true }));
812
+ return;
813
+ } catch (error) {
814
+ if (error.code !== "ENOENT" && error.code !== "privacy_model_integrity_failed") throw error;
815
+ }
816
+ const space = await statfs(directory);
817
+ if (Number(space.bavail) * Number(space.bsize) < 4 * 2 ** 30) throw new Error("Keep at least 4 GiB free for installation and safe updates.");
818
+ const partial = join2(directory, "download.part");
819
+ try {
820
+ const stage = await lstat(partial);
821
+ if (!stage.isFile() || stage.isSymbolicLink() || stage.mode & 63 || process.getuid && stage.uid !== process.getuid()) throw privacyError("privacy_unsafe_download");
822
+ } catch (error) {
823
+ if (error.code !== "ENOENT") throw error;
824
+ }
825
+ if (options.modelFile) {
826
+ await copyFile(resolve2(options.modelFile), partial);
827
+ await chmod(partial, 384);
828
+ } else {
829
+ let received = 0;
830
+ try {
831
+ const s = await lstat(partial);
832
+ if (!s.isFile() || s.isSymbolicLink() || s.mode & 63) throw privacyError("privacy_unsafe_download");
833
+ received = s.size;
834
+ } catch (error) {
835
+ if (error.code !== "ENOENT") throw error;
836
+ }
837
+ if (received > PRIVACY_MODEL.bytes) {
838
+ await rm(partial);
839
+ received = 0;
840
+ }
841
+ if (received < PRIVACY_MODEL.bytes) {
842
+ const response = await fetch(PRIVACY_MODEL.url, { headers: received ? { Range: `bytes=${received}-` } : {}, signal: AbortSignal.timeout(30 * 6e4) });
843
+ if (!(response.status === 200 || response.status === 206) || !response.body) throw new Error("Model download failed. Retry to resume it.");
844
+ if (response.status === 200) received = 0;
845
+ if (response.status === 206 && !response.headers.get("content-range")?.startsWith(`bytes ${received}-`)) throw privacyError("privacy_invalid_download_range");
846
+ const file = await open(partial, received ? "a" : "w", 384);
847
+ try {
848
+ for await (const chunk of response.body) {
849
+ received += chunk.length;
850
+ if (received > PRIVACY_MODEL.bytes) throw privacyError("privacy_invalid_download_size");
851
+ await file.writeFile(chunk);
852
+ options.progress?.(received, PRIVACY_MODEL.bytes);
853
+ }
854
+ await file.sync();
855
+ } finally {
856
+ await file.close();
857
+ }
858
+ }
859
+ }
860
+ if ((await lstat(partial)).size !== PRIVACY_MODEL.bytes || await fileDigest(partial) !== PRIVACY_MODEL.sha256) {
861
+ await rm(partial, { force: true });
862
+ throw privacyError("privacy_model_integrity_failed");
863
+ }
864
+ await rename(partial, privacyModelPath());
865
+ await writePrivateJson(join2(directory, "installation.json"), { sha256: PRIVACY_MODEL.sha256, bytes: PRIVACY_MODEL.bytes, revision: PRIVACY_MODEL.revision });
866
+ if (!options.downloadOnly) await updatePrivacyPreferences((p) => ({ ...p, enabled: true, offerSeen: true }));
867
+ } finally {
868
+ await release();
869
+ }
870
+ }
871
+
872
+ // src/privacyWorker.ts
873
+ var MAX_TEXT = 1024 * 1024;
874
+ var MAX_WIRE = 8 * MAX_TEXT;
875
+ var TIMEOUT = 5 * 6e4;
876
+ var NativeWorker = class {
877
+ child;
878
+ ready;
879
+ buffer = Buffer.alloc(0);
880
+ waiting = [];
881
+ failed = false;
882
+ loaded = false;
883
+ constructor(executable) {
884
+ this.child = spawn(executable, [privacyModelPath(), String(Math.max(1, Math.min(4, availableParallelism2() - 1)))], {
885
+ cwd: privacyRoot(),
886
+ env: { LANG: "C.UTF-8", LD_LIBRARY_PATH: privacyRuntimeDirectory() },
887
+ stdio: "pipe"
888
+ });
889
+ this.child.stderr.resume();
890
+ this.ready = this.frame().then((reply) => {
891
+ this.loaded = reply.ready === true;
892
+ return reply;
893
+ });
894
+ this.child.stdout.on("data", (chunk) => {
895
+ this.buffer = Buffer.concat([this.buffer, chunk]);
896
+ while (this.buffer.length >= 4) {
897
+ const size = this.buffer.readUInt32BE(0);
898
+ if (size > MAX_WIRE) {
899
+ this.fail();
900
+ return;
901
+ }
902
+ if (this.buffer.length < size + 4) break;
903
+ const payload = this.buffer.subarray(4, size + 4);
904
+ this.buffer = this.buffer.subarray(size + 4);
905
+ const next = this.waiting.shift();
906
+ if (!next) {
907
+ this.fail();
908
+ return;
909
+ }
910
+ try {
911
+ next.resolve(JSON.parse(payload.toString("utf8")));
912
+ } catch {
913
+ next.reject(privacyError("privacy_worker_protocol_failed"));
914
+ this.fail();
915
+ }
916
+ }
917
+ });
918
+ this.child.on("error", () => this.fail());
919
+ this.child.on("exit", () => this.fail());
920
+ }
921
+ frame() {
922
+ if (this.failed) return Promise.reject(privacyError("privacy_worker_failed"));
923
+ return new Promise((resolve3, reject) => this.waiting.push({ resolve: resolve3, reject }));
924
+ }
925
+ fail() {
926
+ this.failed = true;
927
+ this.child.kill("SIGKILL");
928
+ for (const waiter of this.waiting.splice(0)) waiter.reject(privacyError("privacy_worker_failed"));
929
+ }
930
+ async scan(text) {
931
+ if (!(await this.ready).ready) throw privacyError("privacy_worker_protocol_failed");
932
+ const payload = Buffer.from(text, "utf8");
933
+ const header = Buffer.alloc(4);
934
+ header.writeUInt32BE(payload.length);
935
+ const pending = this.frame();
936
+ this.child.stdin.write(Buffer.concat([header, payload]));
937
+ const reply = await pending;
938
+ if (reply.error || !Array.isArray(reply.spans)) throw privacyError("privacy_worker_failed");
939
+ return reply.spans;
940
+ }
941
+ };
942
+ async function privacyRpc(request, timeout = TIMEOUT) {
943
+ const path = privacySocketPath();
944
+ const info = await lstat2(path);
945
+ if (!info.isSocket() || info.mode & 63 || process.getuid && info.uid !== process.getuid()) throw privacyError("privacy_unsafe_socket");
946
+ return new Promise((resolveReply, reject) => {
947
+ const socket = createConnection(path);
948
+ let data = "";
949
+ const timer = setTimeout(() => {
950
+ socket.destroy();
951
+ reject(privacyError("privacy_scan_timed_out"));
952
+ }, timeout);
953
+ const finish = (error, reply) => {
954
+ clearTimeout(timer);
955
+ socket.destroy();
956
+ if (error) reject(error);
957
+ else resolveReply(reply);
958
+ };
959
+ socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
960
+ socket.on("data", (chunk) => {
961
+ data += chunk.toString("utf8");
962
+ if (Buffer.byteLength(data) > MAX_WIRE) {
963
+ finish(privacyError("privacy_worker_protocol_failed"));
964
+ return;
965
+ }
966
+ const end = data.indexOf("\n");
967
+ if (end === -1) return;
968
+ try {
969
+ const reply = JSON.parse(data.slice(0, end));
970
+ finish(reply.error ? privacyError(reply.error) : void 0, reply);
971
+ } catch {
972
+ finish(privacyError("privacy_worker_protocol_failed"));
973
+ }
974
+ });
975
+ socket.on("error", () => finish(privacyError("privacy_worker_unavailable")));
976
+ socket.on("end", () => {
977
+ if (!data.includes("\n")) finish(privacyError("privacy_worker_unavailable"));
978
+ });
979
+ });
980
+ }
981
+ async function ensurePrivacyDaemon() {
982
+ try {
983
+ const reply = await privacyRpc({ op: "status" }, 1e3);
984
+ if (reply.protocol === 1 && reply.model === PRIVACY_MODEL.sha256) return;
985
+ } catch {
986
+ }
987
+ if (!(await privacyStatus()).installed) throw privacyError("privacy_model_not_installed");
988
+ const root = privacyRoot();
989
+ await privateDirectory(root);
990
+ const unlock = await lockfile3.lock(root, { lockfilePath: root + "/worker-start.lock", stale: 3e4, retries: { retries: 40, minTimeout: 100, maxTimeout: 200 } });
991
+ try {
992
+ try {
993
+ const reply = await privacyRpc({ op: "status" }, 1e3);
994
+ if (reply.protocol === 1 && reply.model === PRIVACY_MODEL.sha256) return;
995
+ await stopPrivacyDaemon();
996
+ } catch {
997
+ }
998
+ await rm2(privacySocketPath(), { force: true });
999
+ const entry = fileURLToPath2(new URL("./privacyDaemon.js", import.meta.url));
1000
+ const child = spawn(process.execPath, [entry], { detached: true, stdio: "ignore", env: { PATH: process.env.PATH, LANG: "C.UTF-8", OPENMATES_PRIVACY_DIR: root } });
1001
+ child.unref();
1002
+ const start = Date.now();
1003
+ while (Date.now() - start < 1e4) {
1004
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
1005
+ try {
1006
+ const reply = await privacyRpc({ op: "status" }, 1e3);
1007
+ if (reply.protocol === 1 && reply.model === PRIVACY_MODEL.sha256) return;
1008
+ await stopPrivacyDaemon();
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ throw privacyError("privacy_worker_unavailable");
1013
+ } finally {
1014
+ await unlock();
1015
+ }
1016
+ }
1017
+ async function stopPrivacyDaemon() {
1018
+ try {
1019
+ await privacyRpc({ op: "stop" }, 1e3);
1020
+ } catch {
1021
+ }
1022
+ for (let i = 0; i < 20; i++) {
1023
+ try {
1024
+ await lstat2(privacySocketPath());
1025
+ } catch {
1026
+ return;
1027
+ }
1028
+ await new Promise((done) => setTimeout(done, 50));
1029
+ }
1030
+ }
1031
+ async function runPrivacyDaemon() {
1032
+ await privateDirectory(privacyRoot());
1033
+ let native = null;
1034
+ let initializing = null;
1035
+ const cache = /* @__PURE__ */ new Map();
1036
+ let tail = Promise.resolve();
1037
+ let queued = 0;
1038
+ let pendingBytes = 0;
1039
+ let idle;
1040
+ const close = async () => {
1041
+ native?.fail();
1042
+ cache.clear();
1043
+ server.close();
1044
+ await rm2(privacySocketPath(), { force: true });
1045
+ process.exit(0);
1046
+ };
1047
+ const armIdle = (seconds = 120) => {
1048
+ clearTimeout(idle);
1049
+ idle = setTimeout(() => {
1050
+ if (!queued) void close();
1051
+ }, seconds * 1e3);
1052
+ };
1053
+ const getNative = () => initializing ??= (async () => {
1054
+ const memory = await readFile2("/proc/meminfo", "utf8").catch(() => "");
1055
+ const available = Number(/^MemAvailable:\s+(\d+)/m.exec(memory)?.[1] ?? 0) * 1024 || freemem();
1056
+ if (available < 2.5 * 2 ** 30) throw privacyError("privacy_insufficient_available_memory");
1057
+ await verifyInstalledModel();
1058
+ const executable = await verifyPrivacyRuntime();
1059
+ native = new NativeWorker(executable);
1060
+ await native.ready;
1061
+ return native;
1062
+ })();
1063
+ const watchdog = setInterval(() => {
1064
+ if (!native?.child.pid) return;
1065
+ void readFile2(`/proc/${native.child.pid}/status`, "utf8").then((data) => {
1066
+ const rss = Number(/^VmRSS:\s+(\d+)/m.exec(data)?.[1] ?? 0) * 1024;
1067
+ if (rss > 3 * 2 ** 30) native?.fail();
1068
+ }).catch(() => {
1069
+ });
1070
+ }, 1e3);
1071
+ watchdog.unref();
1072
+ const server = createServer((socket) => {
1073
+ let data = Buffer.alloc(0);
1074
+ let accepted = false;
1075
+ socket.setTimeout(TIMEOUT, () => socket.destroy());
1076
+ socket.on("error", () => {
1077
+ });
1078
+ socket.on("data", (chunk) => {
1079
+ if (accepted) return;
1080
+ data = Buffer.concat([data, chunk]);
1081
+ if (data.length > MAX_WIRE) {
1082
+ socket.destroy();
1083
+ return;
1084
+ }
1085
+ const end = data.indexOf(10);
1086
+ if (end < 0) return;
1087
+ accepted = true;
1088
+ let request;
1089
+ try {
1090
+ request = JSON.parse(data.subarray(0, end).toString("utf8"));
1091
+ } catch {
1092
+ socket.destroy();
1093
+ return;
1094
+ }
1095
+ data.fill(0);
1096
+ data = Buffer.alloc(0);
1097
+ if (request.op === "status") {
1098
+ socket.end(JSON.stringify({ protocol: 1, model: PRIVACY_MODEL.sha256, state: native?.failed ? "failed" : native?.loaded ? "ready" : initializing ? "loading" : "installed_enabled" }) + "\n");
1099
+ return;
1100
+ }
1101
+ if (request.op === "stop") {
1102
+ socket.end('{"state":"stopped"}\n');
1103
+ void close();
1104
+ return;
1105
+ }
1106
+ if (request.op !== "scan" || typeof request.text !== "string" || !/^[a-f0-9]{64}$/.test(request.scope ?? "") || Buffer.byteLength(request.text) > MAX_TEXT || queued >= 8 || pendingBytes + Buffer.byteLength(request.text) > 4 * MAX_TEXT) {
1107
+ socket.end('{"error":"privacy_request_rejected"}\n');
1108
+ return;
1109
+ }
1110
+ const text = request.text;
1111
+ request.text = void 0;
1112
+ const bytes = Buffer.byteLength(text);
1113
+ pendingBytes += bytes;
1114
+ queued++;
1115
+ clearTimeout(idle);
1116
+ const key = request.scope + ":" + createHash4("sha256").update(text).digest("hex");
1117
+ tail = tail.catch(() => {
1118
+ }).then(async () => {
1119
+ try {
1120
+ let spans = cache.get(key);
1121
+ if (!spans) {
1122
+ spans = await (await getNative()).scan(text);
1123
+ if (cache.size >= 128) cache.delete(cache.keys().next().value);
1124
+ cache.set(key, spans);
1125
+ }
1126
+ socket.end(JSON.stringify({ spans }) + "\n");
1127
+ } catch {
1128
+ socket.end('{"error":"privacy_scan_failed"}\n');
1129
+ } finally {
1130
+ queued--;
1131
+ pendingBytes -= bytes;
1132
+ armIdle(Math.max(15, Math.min(3600, request.idleSeconds || 120)));
1133
+ }
1134
+ });
1135
+ });
1136
+ });
1137
+ await new Promise((resolveListen, reject) => {
1138
+ server.once("error", reject);
1139
+ server.listen(privacySocketPath(), () => {
1140
+ void chmod2(privacySocketPath(), 384).then(resolveListen, reject);
1141
+ });
1142
+ });
1143
+ armIdle();
1144
+ process.once("SIGTERM", () => {
1145
+ void close();
1146
+ });
1147
+ process.once("SIGINT", () => {
1148
+ void close();
1149
+ });
1150
+ }
1151
+
1152
+ export {
1153
+ resolveStateDir,
1154
+ loadAnonymousId,
1155
+ saveAnonymousId,
1156
+ withSessionRefreshLock,
1157
+ saveSession,
1158
+ loadSession,
1159
+ getCredentialStorageMode,
1160
+ purgeLocalPrivateData,
1161
+ saveLocalTeamKey,
1162
+ loadLocalTeamKey,
1163
+ pruneLocalTeamArtifacts,
1164
+ saveSyncCache,
1165
+ loadSyncCache,
1166
+ clearSyncCache,
1167
+ isSyncCacheFresh,
1168
+ loadTrustedAccountId,
1169
+ saveTrustedAccountId,
1170
+ assertTrustedAccountId,
1171
+ ENHANCED_ANONYMIZATION_LABEL,
1172
+ privacyModelPath,
1173
+ privacyProfile,
1174
+ privacyError,
1175
+ readPrivacyPreferences,
1176
+ updatePrivacyPreferences,
1177
+ enhancedScopeEnabled,
1178
+ privacyCapability,
1179
+ verifyInstalledModel,
1180
+ verifyPrivacyRuntime,
1181
+ privacyStatus,
1182
+ claimPrivacyOffer,
1183
+ installPrivacyModel,
1184
+ privacyRpc,
1185
+ ensurePrivacyDaemon,
1186
+ stopPrivacyDaemon,
1187
+ runPrivacyDaemon
1188
+ };