aegis-desktop 1.0.13 → 1.0.15

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.
Files changed (96) hide show
  1. package/SOURCE-OFFER.txt +9 -0
  2. package/bin/aegis.js +1 -32
  3. package/lib/account-key.js +1 -56
  4. package/lib/auto.js +1 -430
  5. package/lib/avatar/companion.js +1 -698
  6. package/lib/avatar/cosmetics.js +1 -532
  7. package/lib/avatar/events.js +1 -344
  8. package/lib/avatar/holder-turn.js +1 -350
  9. package/lib/avatar/holders.js +1 -612
  10. package/lib/avatar/identity.js +1 -641
  11. package/lib/avatar/level.js +1 -368
  12. package/lib/avatar/mirror.js +1 -524
  13. package/lib/avatar/persona.js +1 -388
  14. package/lib/avatar/profile.js +1 -977
  15. package/lib/avatar/register.js +1 -307
  16. package/lib/avatar/store.js +1 -472
  17. package/lib/avatar/tiers.js +1 -38
  18. package/lib/avatar/transfer.js +1 -415
  19. package/lib/avatar/turn.js +1 -216
  20. package/lib/avatar/voice.js +1 -328
  21. package/lib/avatar/xp.js +1 -404
  22. package/lib/deep-link.js +1 -120
  23. package/lib/fingerprint.js +2 -347
  24. package/lib/google-connect.js +1 -169
  25. package/lib/local/agents.js +1 -105
  26. package/lib/local/autonomous.js +1 -1045
  27. package/lib/local/claude-pro.js +1 -993
  28. package/lib/local/context.js +1 -95
  29. package/lib/local/engine.js +1 -3228
  30. package/lib/local/entitlement.js +1 -654
  31. package/lib/local/git-scope.js +1 -228
  32. package/lib/local/local.js +1 -2135
  33. package/lib/local/prompt.js +1 -275
  34. package/lib/local/queue.js +1 -560
  35. package/lib/local/session-rounds.js +1 -208
  36. package/lib/local/shell.js +1 -680
  37. package/lib/local/telemetry.js +1 -470
  38. package/lib/local/tools.js +1 -1238
  39. package/lib/local/turn-guard.js +1 -300
  40. package/lib/local/worktree-lock.js +1 -235
  41. package/lib/provider-key-mirror.js +1 -252
  42. package/lib/quick-launcher.js +1 -84
  43. package/lib/settings.js +1 -890
  44. package/lib/sync/memory-queue.js +1 -60
  45. package/lib/sync/persist-gate.js +1 -148
  46. package/lib/sync/sessions.js +1 -106
  47. package/lib/window-state.js +1 -66
  48. package/main.js +3 -4390
  49. package/package.json +3 -5
  50. package/preload.js +3 -628
  51. package/renderer/app.js +1 -6560
  52. package/renderer/avatar/assemble.js +1 -335
  53. package/renderer/avatar/avatar.js +1 -380
  54. package/renderer/avatar/cards.js +1 -141
  55. package/renderer/avatar/hud.js +1 -206
  56. package/renderer/avatar/machine.js +1 -213
  57. package/renderer/avatar/pane.js +1 -439
  58. package/renderer/avatar/parts.js +1 -437
  59. package/renderer/budget.js +1 -168
  60. package/renderer/companion.js +1 -223
  61. package/renderer/diff.js +1 -359
  62. package/renderer/diffview.js +1 -336
  63. package/renderer/fingerprint.js +3 -720
  64. package/renderer/markdown.js +1 -168
  65. package/renderer/provider-toolbar.js +1 -134
  66. package/renderer/providers.js +1 -259
  67. package/renderer/quick.js +1 -191
  68. package/renderer/stream-policy.js +1 -306
  69. package/renderer/transcript-view.js +1 -323
  70. package/renderer/usage.js +1 -1239
  71. package/vendor/aegis.js +1 -2054
  72. package/vendor/attest.js +1 -442
  73. package/vendor/authorship-key.js +1 -410
  74. package/vendor/auto-provision.js +1 -134
  75. package/vendor/brain-catalog.js +1 -268
  76. package/vendor/byok-catalog.js +1 -230
  77. package/vendor/credentials.js +1 -494
  78. package/vendor/device-fingerprint.js +1 -141
  79. package/vendor/drive-connect.js +1 -85
  80. package/vendor/drive-upload.js +1 -174
  81. package/vendor/env-file.js +1 -1273
  82. package/vendor/fingerprint.js +1 -1064
  83. package/vendor/foreign-memory.js +1 -669
  84. package/vendor/google-signin.js +1 -125
  85. package/vendor/hooks.js +1 -430
  86. package/vendor/model-discovery.js +1 -330
  87. package/vendor/platform.js +1 -937
  88. package/vendor/prefs.js +1 -351
  89. package/vendor/presence-constants.js +1 -107
  90. package/vendor/presence.js +1 -438
  91. package/vendor/record-vault.js +3 -1055
  92. package/vendor/session-store.js +1 -548
  93. package/vendor/topup.js +1 -297
  94. package/vendor/touch.js +1 -458
  95. package/vendor/update.js +1 -704
  96. package/vendor/user-models.js +1 -381
package/vendor/prefs.js CHANGED
@@ -1,353 +1,3 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
- // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
- // Conveying a modified copy, or running one as a network service, owes the
5
- // corresponding source under this licence (§5/§13) — see LICENSING.md.
6
- 'use strict';
7
-
8
- /**
9
- * prefs.js — the ONE shared, non-secret preference store (AEGISONE, decision
10
- * D5: "one store for preferences; secrets stay where the OS can decrypt them").
11
- *
12
- * Both faces read and write this file:
13
- *
14
- * the window desktop/lib/settings.js (the memory-persist preference, and the
15
- * CLI-key accessors `getSharedPref`/
16
- * `setSharedPref` the store exposes)
17
- * the shell cli/src/config.js (theme, model/class pin, effort, vim,
18
- * memoryPersist)
19
- *
20
- * and `desktop/lib/sync/persist-gate.js` reads it directly, in the main process,
21
- * before it opens a socket.
22
- *
23
- * WHY A THIRD FILE, when the repo already has credentials.json and sessions.json
24
- * in the same directory: those two are a *secret* store and a *content* store.
25
- * This one is neither. Splitting the desktop's single `<userData>/settings.json`
26
- * along the line D5 already drew for sessions and credentials is what makes the
27
- * split honest:
28
- *
29
- * $AEGISCODE_HOME/preferences.json non-secret preferences (this file)
30
- * <userData>/settings.json host-local: `safeStorage` blobs (a plain
31
- * Node process, i.e. the CLI, cannot decrypt
32
- * those on Linux) + machine/UI state
33
- *
34
- * The boundary is enforced, not documented: `set()` refuses to write anything
35
- * that looks like key material (`scanForSecrets`/`assertNoSecrets` below), so a
36
- * caller that tries to park an API key in the shared store gets a thrown error
37
- * rather than a plaintext secret in a file the other host reads. A preference
38
- * store that *can* carry a key is a second place to leak one; this one cannot.
39
- *
40
- * Adoption of a pre-unification `<userData>/settings.json` is one-way, once and
41
- * idempotent — the pattern `client/session-store.js` already proves and
42
- * `test/shared-store.test.mjs` property D already asserts. There is no merge
43
- * loop and no last-writer-wins ping-pong: shared values are never copied back
44
- * into the legacy file, and a second adoption attempt is refused.
45
- */
46
-
47
- const fs = require('node:fs');
48
- const path = require('node:path');
49
-
50
- // The data dir is defined once, in the shared credential module — the same rule
51
- // cli/src/config.js and desktop/lib/sync/sessions.js follow, so the two faces
52
- // cannot drift about where the store lives (`$AEGISCODE_HOME`, else
53
- // `~/.aegiscode`).
54
- const { aegisHome } = require('./credentials.js');
55
-
56
- const PREFS_FILE = 'preferences.json';
57
- const PREFS_VERSION = 1;
58
-
59
- /**
60
- * The non-secret preference keys the shell's `config.js` keeps in the shared
61
- * store (everything else in its config — `updateCheck`, `lastCwd`, a legacy
62
- * `provider`/`baseURL` row — stays in `config.json`, which is host-local).
63
- *
64
- * This list is also the access-control list for the desktop's generic
65
- * `getSharedPref`/`setSharedPref`, so the window can read exactly the
66
- * preferences the shell owns and not reach into anyone else's state.
67
- */
68
- const SHARED_PREF_KEYS = Object.freeze([
69
- 'themeIndex',
70
- 'model',
71
- 'modelClass',
72
- 'currentModelId',
73
- 'models',
74
- 'effort',
75
- 'vim',
76
- 'memoryPersist',
77
- // ON8 / D8 rule 1: the confirmation gate is a POSTURE, not a surface. The
78
- // window's "confirm before running tools" switch and the shell's rule set
79
- // answer the same question ("may this call run unattended?"), so the switch
80
- // lives beside the rules that gate answers, in the one store both hosts read.
81
- 'confirmMode',
82
- ]);
83
-
84
- /**
85
- * The legacy desktop namespace D5 retires: `<userData>/settings.json`'s
86
- * `__memoryPersist: { enabled }` becomes the shared store's `memoryPersist`
87
- * boolean — the shell's own spelling (`cli/src/config.js` `MEMORY_PERSIST_KEY`),
88
- * so one preference has one representation instead of two that could disagree.
89
- */
90
- const LEGACY_MEMORY_PERSIST_NAMESPACE = '__memoryPersist';
91
-
92
- /**
93
- * The second legacy namespace this store retires (ON8): the window's
94
- * `__confirmMode: { enabled }` becomes the shared `confirmMode` boolean. Same
95
- * rule, same reason as memory-persist above — and it is the one that closes
96
- * D8's rule 1 in the direction that used to be impossible: a posture set in
97
- * the window is now the posture the shell reads.
98
- */
99
- const LEGACY_CONFIRM_MODE_NAMESPACE = '__confirmMode';
100
-
101
- /**
102
- * Key names that mean "secret" wherever they appear in the tree. Deliberately
103
- * conservative: `keyMask`-style *previews* never reach this file (they are not
104
- * preferences), and no legitimate preference is named `token`/`secret`/`auth`.
105
- */
106
- const SECRET_KEY_RE =
107
- /(^|[^a-z0-9])(api[_-]?key|apikey|key|keys|token|secret|password|passwd|credential|credentials|authorization|bearer|private[_-]?key|access[_-]?key|session[_-]?key)([^a-z0-9]|$)/i;
108
-
109
- /**
110
- * Values that are key material whatever they are called. Every shape here is a
111
- * real prefix this product's hosts mint or accept: AEGIS Cloud keys
112
- * (`aegis_…`), OpenAI/BYOK keys (`sk-…`), GitHub/Anthropic strings (`ghp_`,
113
- * `sk-ant-…`), JWTs (`eyJ…`, which is what an OAuth-ish provider returns), and
114
- * a bare long base64 blob (which is what Electron's `safeStorage` ciphertext —
115
- * and a base64-encoded key, the exact fallback `settings.js` uses when
116
- * `safeStorage` is unavailable — looks like).
117
- */
118
- const SECRET_VALUE_RES = [
119
- /\baegis_[A-Za-z0-9_\-]{12,}/,
120
- /\bsk-[A-Za-z0-9_\-]{12,}/,
121
- /\bghp_[A-Za-z0-9_\-]{20,}/,
122
- /\bxox[baprs]-[A-Za-z0-9\-]{10,}/,
123
- /\beyJ[A-Za-z0-9_\-]{16,}\.[A-Za-z0-9_\-]{8,}/,
124
- /^[A-Za-z0-9+/]{48,}={0,2}$/,
125
- ];
126
-
127
- /** The shared dir: an explicit `dir`, else `$AEGISCODE_HOME`/`~/.aegiscode`. */
128
- function prefsDir(opts) {
129
- const o = opts || {};
130
- if (o.dir && String(o.dir).trim()) return path.resolve(String(o.dir).trim());
131
- return aegisHome(o.env);
132
- }
133
-
134
- /** The shared file. `prefsFile({ dir, env })`. */
135
- function prefsFile(opts) {
136
- const o = opts || {};
137
- if (o.file && String(o.file).trim()) return path.resolve(String(o.file).trim());
138
- return path.join(prefsDir(o), PREFS_FILE);
139
- }
140
-
141
- /**
142
- * Every finding in `value` that looks like key material, as dotted paths.
143
- * An empty array means "nothing key-shaped in here" — the property the exit
144
- * criteria assert about the shared file, with a negative control that shows the
145
- * scan can fail.
146
- */
147
- function scanForSecrets(value, atPath = '', found = []) {
148
- if (value == null) return found;
149
- if (typeof value === 'string') {
150
- if (SECRET_VALUE_RES.some((re) => re.test(value))) found.push(atPath || '(root)');
151
- return found;
152
- }
153
- if (typeof value !== 'object') return found;
154
- if (Array.isArray(value)) {
155
- value.forEach((v, i) => scanForSecrets(v, `${atPath}[${i}]`, found));
156
- return found;
157
- }
158
- for (const [k, v] of Object.entries(value)) {
159
- const here = atPath ? `${atPath}.${k}` : k;
160
- // An empty/absent value is not key material — `{ key: null }` is how this
161
- // repo clears a stored credential, and refusing that would be noise.
162
- if (SECRET_KEY_RE.test(k) && v != null && v !== '' && v !== false) {
163
- found.push(here);
164
- continue;
165
- }
166
- scanForSecrets(v, here, found);
167
- }
168
- return found;
169
- }
170
-
171
- /** Throw when `value` carries key material. The writer's fail-closed gate. */
172
- function assertNoSecrets(value, what = 'shared preference store') {
173
- const found = scanForSecrets(value);
174
- if (found.length) {
175
- throw new Error(
176
- `${what} must not carry key material — refusing to write (found: ${found.join(', ')}). ` +
177
- 'Secrets belong in the host-local credential store, where the OS can protect them.'
178
- );
179
- }
180
- return value;
181
- }
182
-
183
- function readJson(file, fallback) {
184
- try {
185
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
186
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
187
- } catch {
188
- return fallback;
189
- }
190
- }
191
-
192
- /** Read the shared store. Never throws: a corrupt file reads as empty. */
193
- function load(opts) {
194
- return readJson(prefsFile(opts), {});
195
- }
196
-
197
- /**
198
- * Write the shared store atomically and 0600.
199
- *
200
- * 0600 rather than 0644 even though nothing secret belongs here: the file
201
- * decides whether a user's turns are persisted to the cloud (`memoryPersist`)
202
- * and which provider row the local class dials, so a writable copy is another
203
- * local account's way to change how this host behaves. The mode is the control;
204
- * the scan above is the boundary.
205
- */
206
- function write(next, opts) {
207
- assertNoSecrets(next);
208
- const file = prefsFile(opts);
209
- fs.mkdirSync(path.dirname(file), { recursive: true });
210
- const tmp = `${file}.tmp-${process.pid}`;
211
- fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
212
- fs.renameSync(tmp, file);
213
- try {
214
- fs.chmodSync(file, 0o600);
215
- } catch {
216
- /* Windows: POSIX modes are a no-op, ACLs are the OS's business */
217
- }
218
- return next;
219
- }
220
-
221
- /** Merge a patch into the shared store and return the merged view. */
222
- function set(patch, opts) {
223
- const next = { ...load(opts), ...(patch || {}) };
224
- // `undefined` in a patch is how callers retire a key (JSON.stringify drops
225
- // it), so drop those before the scan and the write.
226
- for (const k of Object.keys(next)) if (next[k] === undefined) delete next[k];
227
- if (next.version === undefined) next.version = PREFS_VERSION;
228
- return write(next, opts);
229
- }
230
-
231
- /** One shared preference value, or `fallback`. */
232
- function get(key, opts, fallback) {
233
- const v = load(opts)[key];
234
- return v === undefined ? fallback : v;
235
- }
236
-
237
- /** True when the store holds an explicit value for `key`. */
238
- function has(key, opts) {
239
- return load(opts)[key] !== undefined;
240
- }
241
-
242
- /**
243
- * Retire a key. Confined here (read-modify-write) so the two faces never
244
- * clobber each other's namespaces with a whole-file overwrite.
245
- */
246
- function remove(key, opts) {
247
- const next = load(opts);
248
- delete next[key];
249
- return write(next, opts);
250
- }
251
-
252
- /**
253
- * The non-secret half of a legacy `<userData>/settings.json`, in the shared
254
- * store's spellings. Only namespaces THIS store owns are extracted: the other
255
- * app-level namespaces (`__avatar`, `__telemetry`,
256
- * `__autoDrain`, `__autoMint`, `__quickLauncher`) stay host-local until their
257
- * own phase moves them, and the provider rows are never here at all because a
258
- * provider row is where a key lives.
259
- */
260
- function extractNonSecretHalf(legacy) {
261
- const out = {};
262
- const src = (legacy || {})[LEGACY_MEMORY_PERSIST_NAMESPACE];
263
- if (src && typeof src === 'object' && typeof src.enabled === 'boolean') {
264
- out.memoryPersist = src.enabled;
265
- }
266
- // ON8: the confirmation gate adopts the same way. `enabled: true` means the
267
- // gate is UP, which is the shared `confirmMode: true`; the shared key keeps
268
- // the window's own polarity so an adopting install cannot flip its gate.
269
- const gate = (legacy || {})[LEGACY_CONFIRM_MODE_NAMESPACE];
270
- if (gate && typeof gate === 'object' && typeof gate.enabled === 'boolean') {
271
- out.confirmMode = gate.enabled;
272
- }
273
- return out;
274
- }
275
-
276
- /**
277
- * One-way, once, idempotent adoption of the legacy desktop store's non-secret
278
- * half.
279
- *
280
- * `adopt({ dir, env, legacyDir })`:
281
- * - `dir`/`env` locate the SHARED store;
282
- * - `legacyDir` is the old settings directory (Electron's `<userData>`; main
283
- * passes the same dir its store was built with).
284
- *
285
- * One-way: the legacy file is read, never written — a preference that moved to
286
- * the shared store is not mirrored back. Once: a key the shared store already
287
- * carries is never overwritten, so a second run has nothing left to do and
288
- * returns `adopted:false` without touching the file. That is what makes it
289
- * idempotent rather than a periodic merge.
290
- */
291
- function adopt(opts) {
292
- const o = opts || {};
293
- const shared = prefsFile(o);
294
- const legacyDir = o.legacyDir;
295
- const legacyFile = legacyDir ? path.join(String(legacyDir), 'settings.json') : null;
296
- if (legacyFile && path.resolve(legacyFile) === shared) {
297
- return { adopted: false, keys: [], from: legacyFile, reason: 'shared store is the legacy file' };
298
- }
299
- if (!legacyFile || !fs.existsSync(legacyFile)) {
300
- return { adopted: false, keys: [], from: legacyFile, reason: 'no legacy settings file' };
301
- }
302
- const current = load(o);
303
- const half = extractNonSecretHalf(readJson(legacyFile, {}));
304
- // One-way means missing-only: an explicit shared value outranks the legacy
305
- // one and is never clobbered by it.
306
- const keys = Object.keys(half).filter((k) => current[k] === undefined);
307
- if (!keys.length) {
308
- return {
309
- adopted: false,
310
- keys: [],
311
- from: legacyFile,
312
- reason: Object.keys(half).length
313
- ? 'shared store already carries these preferences'
314
- : 'legacy store has no shared preferences',
315
- };
316
- }
317
- const patch = {};
318
- for (const k of keys) patch[k] = half[k];
319
- // `names`, NOT `keys` (AEGISONE ON8): this ledger is written through `write()`
320
- // below, so it is scanned like everything else — and `SECRET_KEY_RE` matches
321
- // `key`/`keys` by design, because a preference named `keys` IS how key material
322
- // would smuggle itself into a file the desktop and the shell both read. Spelled
323
- // `keys`, this field made `assertNoSecrets` refuse the store's own write, so
324
- // EVERY adoption with something to adopt threw — the whole pre-D5 migration was
325
- // dead, silently (settings.js's best-effort catch turned the throw into "no
326
- // adoption" and no one was told). The scanner is the control here, not the bug;
327
- // a field name is free to change.
328
- patch.adopted = {
329
- from: legacyFile,
330
- at: new Date().toISOString(),
331
- names: keys.slice().sort(),
332
- };
333
- write({ ...current, ...patch }, o);
334
- return { adopted: true, keys: keys.slice().sort(), from: legacyFile };
335
- }
336
-
337
- module.exports = {
338
- PREFS_FILE,
339
- PREFS_VERSION,
340
- SHARED_PREF_KEYS,
341
- LEGACY_MEMORY_PERSIST_NAMESPACE,
342
- prefsDir,
343
- prefsFile,
344
- load,
345
- set,
346
- get,
347
- has,
348
- remove,
349
- scanForSecrets,
350
- assertNoSecrets,
351
- extractNonSecretHalf,
352
- adopt,
353
- };
3
+ "use strict";const fs=require("node:fs"),path=require("node:path"),{aegisHome:aegisHome}=require("./credentials.js"),PREFS_FILE="preferences.json",PREFS_VERSION=1,SHARED_PREF_KEYS=Object.freeze(["themeIndex","model","modelClass","currentModelId","models","effort","vim","memoryPersist","confirmMode"]),LEGACY_MEMORY_PERSIST_NAMESPACE="__memoryPersist",LEGACY_CONFIRM_MODE_NAMESPACE="__confirmMode",SECRET_KEY_RE=/(^|[^a-z0-9])(api[_-]?key|apikey|key|keys|token|secret|password|passwd|credential|credentials|authorization|bearer|private[_-]?key|access[_-]?key|session[_-]?key)([^a-z0-9]|$)/i,SECRET_VALUE_RES=[/\baegis_[A-Za-z0-9_\-]{12,}/,/\bsk-[A-Za-z0-9_\-]{12,}/,/\bghp_[A-Za-z0-9_\-]{20,}/,/\bxox[baprs]-[A-Za-z0-9\-]{10,}/,/\beyJ[A-Za-z0-9_\-]{16,}\.[A-Za-z0-9_\-]{8,}/,/^[A-Za-z0-9+/]{48,}={0,2}$/];function prefsDir(e){const r=e||{};return r.dir&&String(r.dir).trim()?path.resolve(String(r.dir).trim()):aegisHome(r.env)}function prefsFile(e){const r=e||{};return r.file&&String(r.file).trim()?path.resolve(String(r.file).trim()):path.join(prefsDir(r),PREFS_FILE)}function scanForSecrets(e,r="",t=[]){if(null==e)return t;if("string"==typeof e)return SECRET_VALUE_RES.some((r=>r.test(e)))&&t.push(r||"(root)"),t;if("object"!=typeof e)return t;if(Array.isArray(e))return e.forEach(((e,o)=>scanForSecrets(e,`${r}[${o}]`,t))),t;for(const[o,s]of Object.entries(e)){const e=r?`${r}.${o}`:o;SECRET_KEY_RE.test(o)&&null!=s&&""!==s&&!1!==s?t.push(e):scanForSecrets(s,e,t)}return t}function assertNoSecrets(e,r="shared preference store"){const t=scanForSecrets(e);if(t.length)throw new Error(`${r} must not carry key material — refusing to write (found: ${t.join(", ")}). Secrets belong in the host-local credential store, where the OS can protect them.`);return e}function readJson(e,r){try{const t=JSON.parse(fs.readFileSync(e,"utf8"));return t&&"object"==typeof t&&!Array.isArray(t)?t:r}catch{return r}}function load(e){return readJson(prefsFile(e),{})}function write(e,r){assertNoSecrets(e);const t=prefsFile(r);fs.mkdirSync(path.dirname(t),{recursive:!0});const o=`${t}.tmp-${process.pid}`;fs.writeFileSync(o,JSON.stringify(e,null,2)+"\n",{mode:384}),fs.renameSync(o,t);try{fs.chmodSync(t,384)}catch{}return e}function set(e,r){const t={...load(r),...e||{}};for(const e of Object.keys(t))void 0===t[e]&&delete t[e];return void 0===t.version&&(t.version=1),write(t,r)}function get(e,r,t){const o=load(r)[e];return void 0===o?t:o}function has(e,r){return void 0!==load(r)[e]}function remove(e,r){const t=load(r);return delete t[e],write(t,r)}function extractNonSecretHalf(e){const r={},t=(e||{})[LEGACY_MEMORY_PERSIST_NAMESPACE];t&&"object"==typeof t&&"boolean"==typeof t.enabled&&(r.memoryPersist=t.enabled);const o=(e||{})[LEGACY_CONFIRM_MODE_NAMESPACE];return o&&"object"==typeof o&&"boolean"==typeof o.enabled&&(r.confirmMode=o.enabled),r}function adopt(e){const r=e||{},t=prefsFile(r),o=r.legacyDir,s=o?path.join(String(o),"settings.json"):null;if(s&&path.resolve(s)===t)return{adopted:!1,keys:[],from:s,reason:"shared store is the legacy file"};if(!s||!fs.existsSync(s))return{adopted:!1,keys:[],from:s,reason:"no legacy settings file"};const n=load(r),a=extractNonSecretHalf(readJson(s,{})),i=Object.keys(a).filter((e=>void 0===n[e]));if(!i.length)return{adopted:!1,keys:[],from:s,reason:Object.keys(a).length?"shared store already carries these preferences":"legacy store has no shared preferences"};const c={};for(const e of i)c[e]=a[e];return c.adopted={from:s,at:(new Date).toISOString(),names:i.slice().sort()},write({...n,...c},r),{adopted:!0,keys:i.slice().sort(),from:s}}module.exports={PREFS_FILE:PREFS_FILE,PREFS_VERSION:1,SHARED_PREF_KEYS:SHARED_PREF_KEYS,LEGACY_MEMORY_PERSIST_NAMESPACE:LEGACY_MEMORY_PERSIST_NAMESPACE,prefsDir:prefsDir,prefsFile:prefsFile,load:load,set:set,get:get,has:has,remove:remove,scanForSecrets:scanForSecrets,assertNoSecrets:assertNoSecrets,extractNonSecretHalf:extractNonSecretHalf,adopt:adopt};
@@ -1,109 +1,3 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
- // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
- // Conveying a modified copy, or running one as a network service, owes the
5
- // corresponding source under this licence (§5/§13) — see LICENSING.md.
6
- 'use strict';
7
-
8
- /**
9
- * The wire contract and the sanitising helpers for the presence beacon
10
- * (client/presence.js).
11
- *
12
- * These live in their own file for one reason: the widths and the app-name
13
- * vocabulary below are not this client's invention, they are aegis1's
14
- * (`_presence_token`, `_presence_app` and `PRESENCE_BEAT_INTERVAL_S` in
15
- * aegis1/app.py, and the `client_presence` table in aegis1/db.py). A test can
16
- * assert them without loading the network module, and if the server widens a
17
- * column the mismatch is visible in one file instead of being spread through
18
- * the beacon's request path.
19
- *
20
- * Kept dependency-free: plain Node, no Electron, no aegis.js — `presence.js`
21
- * requires this, and the tests require both.
22
- */
23
-
24
- /** aegis1: `@app.route("/api/v1/heartbeat", methods=["POST"])`. The v1 path is
25
- * the client beacon; `/api/heartbeat` is the dashboard's own JS ping and is
26
- * deliberately NOT used here — it takes no app/install field and writes no
27
- * presence row. */
28
- const PRESENCE_PATH = '/api/v1/heartbeat';
29
-
30
- /** Fallback cadence, in ms, used until the server answers with its own.
31
- * aegis1's PRESENCE_BEAT_INTERVAL_S is 300 (5 min) as this is written; if the
32
- * two ever disagree the server's value wins at runtime, so this only bounds
33
- * the very first beat. */
34
- const DEFAULT_INTERVAL_MS = 300 * 1000;
35
-
36
- /** One minute. `/api/heartbeat` and `/api/v1/heartbeat` are both
37
- * `@login_required`, and that decorator throttles `_track_request()` to
38
- * 1/min/user — beating faster than this would buy nothing but load. */
39
- const MIN_INTERVAL_MS = 60 * 1000;
40
-
41
- /** One hour: the ceiling the server may ask for, and the ceiling the backoff
42
- * ladder may reach. Past this the panel's "online" window is meaningless. */
43
- const MAX_INTERVAL_MS = 60 * 60 * 1000;
44
-
45
- /** Failures slow the beacon down rather than retrying on the same rhythm. */
46
- const BACKOFF_FACTOR = 2;
47
- const MAX_BACKOFF_MS = 30 * 60 * 1000;
48
-
49
- /** A beacon that has not answered in 20 s is a beacon that is not going to:
50
- * the request body is ~120 bytes. Kept well under MIN_INTERVAL_MS so an
51
- * in-flight beat can never overlap the next scheduled one. */
52
- const REQUEST_TIMEOUT_MS = 20 * 1000;
53
-
54
- /** The app vocabulary, copied EXACTLY from aegis1's `PRESENCE_APPS`
55
- * (aegis1/app.py, right above `_presence_app`). This is not a suggestion: the
56
- * server keeps a name only if it is in that tuple and maps everything else to
57
- * 'other', so a client that sends a name of its own invention is a client the
58
- * operator cannot identify in the panel — silently, with a 200 and no error
59
- * anywhere. `test/cli-presence.test.mjs` parses PRESENCE_APPS out of app.py and
60
- * fails if these two lists drift.
61
- *
62
- * The terminal host sends 'cli' rather than a product name for that reason:
63
- * 'aegiscode' is not in the server's tuple, so it would land as 'other'. */
64
- const APP_NAMES = Object.freeze(['desktop', 'cli', 'mcp', 'extension', 'web']);
65
-
66
- /**
67
- * One wire-safe token: trimmed, control characters and whitespace stripped,
68
- * truncated to the width the server's `_presence_token()` will accept. Returns
69
- * '' for anything that is not a usable string, so a caller can use the empty
70
- * result as "omit this field" rather than sending `null`.
71
- *
72
- * The sanitising is done HERE rather than left to the server on purpose: the
73
- * server sanitises so a bad field cannot 400 the beacon, but a client that
74
- * relies on that ships a body it cannot describe. Truncating at the source
75
- * means what this code sends and what the presence row stores are the same
76
- * string.
77
- */
78
- function saneToken(value, limit) {
79
- try {
80
- if (value == null) return '';
81
- if (typeof value === 'number' && !Number.isFinite(value)) return '';
82
- const text = String(value).replace(/[\u0000-\u001f\u007f\s]+/g, '');
83
- const max = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.floor(Number(limit)) : 64;
84
- return text.slice(0, max);
85
- } catch {
86
- return '';
87
- }
88
- }
89
-
90
- /** A non-negative integer queue depth. aegis1 clamps to [0, 100000] itself;
91
- * clamping here too means the two never disagree about what was sent. */
92
- function saneCount(value) {
93
- const n = Number(value);
94
- if (!Number.isFinite(n) || n <= 0) return 0;
95
- return Math.min(100000, Math.floor(n));
96
- }
97
-
98
- module.exports = {
99
- PRESENCE_PATH,
100
- DEFAULT_INTERVAL_MS,
101
- MIN_INTERVAL_MS,
102
- MAX_INTERVAL_MS,
103
- BACKOFF_FACTOR,
104
- MAX_BACKOFF_MS,
105
- REQUEST_TIMEOUT_MS,
106
- APP_NAMES,
107
- saneToken,
108
- saneCount,
109
- };
3
+ "use strict";const PRESENCE_PATH="/api/v1/heartbeat",DEFAULT_INTERVAL_MS=3e5,MIN_INTERVAL_MS=6e4,MAX_INTERVAL_MS=36e5,BACKOFF_FACTOR=2,MAX_BACKOFF_MS=18e5,REQUEST_TIMEOUT_MS=2e4,APP_NAMES=Object.freeze(["desktop","cli","mcp","extension","web"]);function saneToken(e,n){try{if(null==e)return"";if("number"==typeof e&&!Number.isFinite(e))return"";const t=String(e).replace(/[\u0000-\u001f\u007f\s]+/g,""),_=Number.isFinite(Number(n))&&Number(n)>0?Math.floor(Number(n)):64;return t.slice(0,_)}catch{return""}}function saneCount(e){const n=Number(e);return!Number.isFinite(n)||n<=0?0:Math.min(1e5,Math.floor(n))}module.exports={PRESENCE_PATH:PRESENCE_PATH,DEFAULT_INTERVAL_MS:3e5,MIN_INTERVAL_MS:6e4,MAX_INTERVAL_MS:36e5,BACKOFF_FACTOR:2,MAX_BACKOFF_MS:18e5,REQUEST_TIMEOUT_MS:2e4,APP_NAMES:APP_NAMES,saneToken:saneToken,saneCount:saneCount};