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
@@ -1,440 +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
- * Client presence beacon — the client half of aegis1's `/api/v1/heartbeat`.
10
- *
11
- * WHAT IT IS FOR. aegis1 already had `/api/heartbeat` (the dashboard's own JS
12
- * pinging to bump `users.last_active_at`). What it did NOT have was any way to
13
- * answer "who is running aegiscode right now, which app, which build, how many
14
- * machines?" — a user sitting in a session but not making model calls was
15
- * invisible. `heartbeat_v1()` in aegis1/app.py closes that gap by writing a
16
- * `client_presence` row keyed on (user_id, app, install), and this module is
17
- * the beacon that feeds it. Same for both hosts: the CLI's `aegiscode` binary
18
- * and the desktop's main process require this one file.
19
- *
20
- * THE SERVER CONTRACT (aegis1/app.py `heartbeat_v1`, do not change one side
21
- * without the other):
22
- *
23
- * POST /api/v1/heartbeat X-API-Key: <the user's aegis key>
24
- * {"app": "desktop", "version": "0.8.12", "platform": "linux",
25
- * "install": "9f3c1a2b4d5e6f70", "queue": 2, "autonomous": false}
26
- * -> 200 {"ok": true, "online": true, "seen_at": ..., "beat_interval_s": 300}
27
- *
28
- * Auth is the ordinary `@login_required`, which already accepts X-API-Key or
29
- * Bearer — the same key every host signs in with. So the beacon introduces NO
30
- * second credential and no new client secret, and a host with no key has
31
- * nothing to beacon AT: `apiKey` absent means this module is inert by
32
- * construction (see `disabledPresence()`), never a 401 loop.
33
- *
34
- * `install` IS THE FINGERPRINT. Not a second identifier invented here: it is
35
- * `machineFingerprint()` from ./device-fingerprint.js, the same SHA-256 the
36
- * device-registration path uses. That is deliberate and it is the property the
37
- * admin panel needs — "how many machines does this user run" has to count
38
- * machines, so the beacon must carry the machine's identity, not a random
39
- * per-run token that would make one laptop look like fifty. It is a 64-char
40
- * lowercase hex digest and aegis1's `_presence_token(install, 64)` accepts
41
- * exactly that width. Note what this does and does not imply: it is a stable
42
- * pseudonym for a machine, so a user's own rows join up; it is NOT a
43
- * cross-account tracking device and the server never sees the raw machine id it
44
- * was derived from.
45
- *
46
- * FOUR PROPERTIES, in order of importance — the same discipline
47
- * `lib/local/telemetry.js` is built around, for the same reason:
48
- *
49
- * 1. It cannot break the host. `beat()` returns a boolean and NEVER throws,
50
- * whatever the network, the DNS, the JSON, or an injected `fetch` does.
51
- * A beacon that can take down a CLI session or an Electron main process
52
- * is worse than no beacon at all.
53
- * 2. It cannot leak. The request body is rebuilt field by field from the
54
- * arguments — never spread, never `JSON.stringify(callerObject)` — and
55
- * only `app`, `version`, `platform`, `install`, `queue`, `autonomous` can
56
- * ever be sent. No conversation content, no file paths, no model output,
57
- * no email, no prompt text. `sane()` additionally truncates each field to
58
- * the same width the server enforces, so a caller cannot smuggle a blob
59
- * through a field this module owns.
60
- * 3. It cannot keep the process alive. The timer is `unref()`d, so `stop()`
61
- * is a convenience, not a requirement: an interrupted CLI exits on time
62
- * even if nobody ever calls it, and the desktop's quit path does not have
63
- * to thread a handle through.
64
- * 4. It cannot hammer. The cadence is the SERVER's (`beat_interval_s` in the
65
- * response), so the beacon can be slowed or sped up without shipping a
66
- * client release; failures back off exponentially instead of retrying on
67
- * the same rhythm, which is what turns a dead endpoint from a client
68
- * problem into a server problem.
69
- *
70
- * Nothing in this file may import Electron: it is plain Node so the CLI, the
71
- * desktop main process and the tests can all drive the same code. Each host
72
- * injects its own `app` name, version, platform and queue depth.
73
- */
74
-
75
- const {
76
- PRESENCE_PATH,
77
- DEFAULT_INTERVAL_MS,
78
- MIN_INTERVAL_MS,
79
- MAX_INTERVAL_MS,
80
- BACKOFF_FACTOR,
81
- MAX_BACKOFF_MS,
82
- REQUEST_TIMEOUT_MS,
83
- APP_NAMES,
84
- saneToken,
85
- saneCount,
86
- } = require('./presence-constants.js');
87
-
88
- const DEFAULT_API_BASE = 'https://aegiscloud.org';
89
-
90
- /**
91
- * The store returned when there is nothing to beacon (no key, or a host that
92
- * explicitly opted out). Every method answers, nothing happens, no timer is
93
- * created and no request is made: `enabled` is false and `beat()` is a no-op
94
- * that reports `false`. Checked by test/presence.test.mjs.
95
- */
96
- function disabledPresence(reason) {
97
- const state = {
98
- enabled: false,
99
- reason: String(reason || 'disabled'),
100
- beats: 0,
101
- failures: 0,
102
- lastError: null,
103
- lastSeenAt: null,
104
- intervalMs: null,
105
- install: null,
106
- app: null,
107
- running: false,
108
- };
109
- return {
110
- enabled: false,
111
- reason: state.reason,
112
- install: null,
113
- beat: () => Promise.resolve(false),
114
- start: () => false,
115
- stop: () => false,
116
- state: () => ({ ...state }),
117
- };
118
- }
119
-
120
- /** One ISO-8601 UTC string, or null. Never throws on a hostile clock. */
121
- function isoOf(ms) {
122
- try {
123
- const n = Number(ms);
124
- if (!Number.isFinite(n)) return null;
125
- return new Date(n).toISOString();
126
- } catch {
127
- return null;
128
- }
129
- }
130
-
131
- /**
132
- * @param {object} opts
133
- * @param {string} [opts.apiKey] the aegis key; absent ⇒ inert
134
- * @param {string} [opts.baseUrl] defaults to https://aegiscloud.org
135
- * @param {string} [opts.app] 'desktop' | 'cli' | ... (see APP_NAMES)
136
- * @param {string} [opts.version] the host's own build string
137
- * @param {string} [opts.platform] process.platform
138
- * @param {string} [opts.install] machine fingerprint (default: computed)
139
- * @param {number|function} [opts.queueDepth] pending queue tasks (may be a fn)
140
- * @param {boolean|function} [opts.autonomous] a drain is running (may be a fn)
141
- * @param {number} [opts.intervalMs] initial cadence before the server answers
142
- * @param {boolean} [opts.enabled] explicit kill switch (default true)
143
- * @param {string} [opts.clientVersion] X-AEGIS-Version header (version gate)
144
- * @param {function} [opts.fetchImpl] fetch override (tests)
145
- * @param {function} [opts.now] clock override (tests)
146
- * @param {function} [opts.log] (level, message, err) => void
147
- * @param {function} [opts.setTimer] setTimeout override (tests)
148
- * @param {function} [opts.clearTimer] clearTimeout override (tests)
149
- */
150
- function createPresence(opts = {}) {
151
- const apiKey = typeof opts.apiKey === 'string' ? opts.apiKey.trim() : '';
152
- if (!apiKey) return disabledPresence('no-api-key');
153
- if (opts.enabled === false) return disabledPresence('disabled');
154
-
155
- const log = typeof opts.log === 'function' ? opts.log : () => {};
156
- // A logger that throws is not allowed to matter — same rule as telemetry.js.
157
- function debugOnce(kind) {
158
- if (logged.has(kind)) return;
159
- logged.add(kind);
160
- try {
161
- log('debug', `presence: ${kind}`);
162
- } catch {
163
- /* a broken logger is not an error path */
164
- }
165
- }
166
- const logged = new Set();
167
-
168
- const now = typeof opts.now === 'function' ? opts.now : () => Date.now();
169
- const setTimer = typeof opts.setTimer === 'function' ? opts.setTimer : setTimeout;
170
- const clearTimer = typeof opts.clearTimer === 'function' ? opts.clearTimer : clearTimeout;
171
-
172
- // The fingerprint. Required lazily so a host that never beacons (the tests
173
- // for the other modules) does not touch the filesystem at require time.
174
- //
175
- // An injected `fingerprint` is an IDENTITY SOURCE, not a first choice: when a
176
- // host hands one in, that is the machine's identity and the built-in ladder
177
- // is not consulted. That matters for the packaged hosts, which resolve the
178
- // fingerprint once at their own call site (cli/src/presence.js, desktop
179
- // main.js) and must be able to fail loudly if it cannot — a silent fallback
180
- // here would hide a broken identity by substituting a different one, and the
181
- // panel would count the same laptop as two machines.
182
- let install = saneToken(opts.install, 64);
183
- if (!install && typeof opts.fingerprint === 'function') {
184
- try {
185
- install = saneToken(opts.fingerprint(), 64);
186
- } catch {
187
- debugOnce('fingerprint');
188
- }
189
- } else if (!install) {
190
- try {
191
- install = saneToken(require('./device-fingerprint.js').machineFingerprint(), 64);
192
- } catch {
193
- debugOnce('fingerprint');
194
- }
195
- }
196
- if (!install) {
197
- // No machine identity could be resolved at all (an exotic sandbox). A
198
- // beacon without an install key would write a NEW presence row on every
199
- // beat — one user, infinite machines in the panel. Better to stay out of
200
- // the table than to poison it: see device-fingerprint.js for the ladder
201
- // (machine id -> persisted install id -> hostname) that makes this rare.
202
- return disabledPresence('no-install-identity');
203
- }
204
-
205
- const app = saneToken(opts.app, 24).toLowerCase() || 'other';
206
- const version = saneToken(opts.version, 32);
207
- const rawPlatform = saneToken(opts.platform, 16).toLowerCase();
208
- const platform = rawPlatform || (typeof process !== 'undefined' && process.platform ? process.platform : '');
209
- const baseUrl = String(opts.baseUrl || (typeof process !== 'undefined' && process.env && process.env.AEGIS_API_BASE) || DEFAULT_API_BASE).replace(/\/+$/, '');
210
- const path = PRESENCE_PATH;
211
- const fetchImpl =
212
- typeof opts.fetchImpl === 'function'
213
- ? opts.fetchImpl
214
- : typeof fetch === 'function'
215
- ? fetch
216
- : null;
217
-
218
- /** A caller passing a function is asking to be read at send time, so the
219
- * beacon reports the LIVE queue depth rather than the depth at boot. */
220
- function readDynamic(value, fallback) {
221
- try {
222
- return typeof value === 'function' ? value() : value;
223
- } catch {
224
- return fallback;
225
- }
226
- }
227
-
228
- let intervalMs = clampInterval(opts.intervalMs);
229
- const state = {
230
- enabled: true,
231
- reason: null,
232
- beats: 0,
233
- failures: 0,
234
- lastError: null,
235
- lastSeenAt: null,
236
- intervalMs,
237
- install,
238
- app,
239
- version: version || null,
240
- platform: platform || null,
241
- running: false,
242
- };
243
-
244
- let timer = null;
245
- let inflight = null;
246
- let backoffMs = 0;
247
- let stopped = false;
248
-
249
- function clampInterval(ms) {
250
- const n = Number(ms);
251
- if (!Number.isFinite(n) || n <= 0) return DEFAULT_INTERVAL_MS;
252
- return Math.min(MAX_INTERVAL_MS, Math.max(MIN_INTERVAL_MS, Math.floor(n)));
253
- }
254
-
255
- /** The ONLY body shape this module can ever send. Field by field, on purpose. */
256
- function body() {
257
- return {
258
- app,
259
- version: version || undefined,
260
- platform: platform || undefined,
261
- install,
262
- queue: saneCount(readDynamic(opts.queueDepth, 0)),
263
- autonomous: Boolean(readDynamic(opts.autonomous, false)),
264
- };
265
- }
266
-
267
- /**
268
- * Send one beat. Resolves `true` when the server acknowledged, `false` on
269
- * every other outcome — no key, offline, DNS failure, 500, a malformed body,
270
- * an aborted request. It never rejects, so a caller may `void beat()` without
271
- * an unhandled-rejection guard, and a concurrent call while one is in flight
272
- * is answered by the in-flight promise rather than by a second request (two
273
- * beacons racing would double-count `beats` on the server).
274
- */
275
- function beat() {
276
- if (stopped) return Promise.resolve(false);
277
- if (inflight) return inflight;
278
- if (!fetchImpl) {
279
- debugOnce('no-fetch');
280
- state.lastError = 'no fetch implementation';
281
- state.failures += 1;
282
- return Promise.resolve(false);
283
- }
284
-
285
- const startedAt = now();
286
- inflight = (async () => {
287
- let timedOut = false;
288
- let cancel = null;
289
- try {
290
- const controller = typeof AbortController === 'function' ? new AbortController() : null;
291
- if (controller) {
292
- const t = setTimer(() => {
293
- timedOut = true;
294
- try {
295
- controller.abort();
296
- } catch {
297
- /* already gone */
298
- }
299
- }, REQUEST_TIMEOUT_MS);
300
- if (t && typeof t.unref === 'function') t.unref();
301
- cancel = () => clearTimer(t);
302
- }
303
-
304
- const res = await fetchImpl(`${baseUrl}${path}`, {
305
- method: 'POST',
306
- headers: {
307
- 'Content-Type': 'application/json',
308
- 'X-API-Key': apiKey,
309
- 'X-AEGIS-Version': saneToken(opts.clientVersion, 32) || version || '',
310
- },
311
- body: JSON.stringify(body()),
312
- ...(controller ? { signal: controller.signal } : {}),
313
- });
314
- if (cancel) cancel();
315
-
316
- // `ok` on the response, not `res.ok === true` semantics: a 404 (server
317
- // predates the route) and a 401 (key revoked) are both just "not this
318
- // time" — the beacon must not retry storm either of them.
319
- if (!res || typeof res.status !== 'number' || res.status < 200 || res.status >= 300) {
320
- state.failures += 1;
321
- state.lastError = `http ${res && res.status ? res.status : 'error'}`;
322
- backoffMs = nextBackoff(backoffMs);
323
- return false;
324
- }
325
-
326
- let data = null;
327
- try {
328
- data = await res.json();
329
- } catch {
330
- // A 200 with an unreadable body is still a successful beat: the
331
- // server wrote the row before it serialised the answer. Only the
332
- // cadence hint is lost, and the current one stays in force.
333
- data = null;
334
- }
335
-
336
- state.beats += 1;
337
- state.failures = 0;
338
- state.lastError = null;
339
- state.lastSeenAt = isoOf(now());
340
- backoffMs = 0;
341
- // The server owns the cadence. An unrecognised/hostile value is clamped
342
- // rather than trusted: a `beat_interval_s` of 0 would be a hot loop.
343
- if (data && typeof data === 'object' && data.beat_interval_s != null) {
344
- const secs = Number(data.beat_interval_s);
345
- if (Number.isFinite(secs) && secs > 0) intervalMs = clampInterval(secs * 1000);
346
- }
347
- state.intervalMs = intervalMs;
348
- return true;
349
- } catch (err) {
350
- if (cancel) cancel();
351
- state.failures += 1;
352
- state.lastError = timedOut ? 'timeout' : String((err && err.message) || err || 'error').slice(0, 200);
353
- backoffMs = nextBackoff(backoffMs);
354
- if (state.failures === 1) debugOnce('beat-failed'); // once, then quiet
355
- return false;
356
- } finally {
357
- inflight = null;
358
- if (state.running) schedule(Math.max(0, intervalMs + backoffMs - (now() - startedAt)));
359
- }
360
- })();
361
-
362
- return inflight;
363
- }
364
-
365
- function nextBackoff(current) {
366
- const base = current > 0 ? current * BACKOFF_FACTOR : Math.max(MIN_INTERVAL_MS, intervalMs);
367
- return Math.min(MAX_BACKOFF_MS, Math.floor(base));
368
- }
369
-
370
- /** One timer at a time, always unref'd, always cleared before replacing. */
371
- function schedule(delayMs) {
372
- if (stopped || typeof setTimer !== 'function') return;
373
- if (timer) clearTimer(timer);
374
- const delay = Math.max(1000, Math.floor(Number(delayMs) || intervalMs));
375
- timer = setTimer(() => {
376
- timer = null;
377
- void beat();
378
- }, delay);
379
- if (timer && typeof timer.unref === 'function') timer.unref();
380
- }
381
-
382
- /**
383
- * Start beaconing and send the FIRST beat immediately. A host that is opened
384
- * and closed in under a minute still registers — waiting one full interval
385
- * before the first beat would make the panel blind to exactly the short
386
- * sessions the route was added for.
387
- */
388
- function start() {
389
- if (stopped) return false;
390
- state.running = true;
391
- void beat();
392
- return true;
393
- }
394
-
395
- /** Stop beaconing. Idempotent, and safe to call when start() never ran. */
396
- function stop() {
397
- stopped = true;
398
- state.running = false;
399
- if (timer) {
400
- try {
401
- clearTimer(timer);
402
- } catch {
403
- /* a timer that already fired is not an error */
404
- }
405
- timer = null;
406
- }
407
- return true;
408
- }
409
-
410
- return {
411
- enabled: true,
412
- app,
413
- install,
414
- beat,
415
- start,
416
- stop,
417
- /**
418
- * The in-flight beat, or null. Exposed for one reason: a SHORT-LIVED host
419
- * (`aegiscode -p "…"`, `aegiscode autonomous run`) finishes its work in
420
- * well under a second and calls `process.exit()`, which kills an in-flight
421
- * request and loses the beat entirely — the machine is silently absent from
422
- * the panel even though it just ran. The CLI awaits this before exiting
423
- * (see `flush()` in cli/src/presence.js). A long-lived host can ignore it.
424
- */
425
- pending: () => inflight,
426
- state: () => ({ ...state, intervalMs }),
427
- };
428
- }
429
-
430
- module.exports = {
431
- createPresence,
432
- disabledPresence,
433
- PRESENCE_PATH,
434
- DEFAULT_INTERVAL_MS,
435
- MIN_INTERVAL_MS,
436
- MAX_INTERVAL_MS,
437
- MAX_BACKOFF_MS,
438
- REQUEST_TIMEOUT_MS,
439
- APP_NAMES,
440
- };
3
+ "use strict";const{PRESENCE_PATH:PRESENCE_PATH,DEFAULT_INTERVAL_MS:DEFAULT_INTERVAL_MS,MIN_INTERVAL_MS:MIN_INTERVAL_MS,MAX_INTERVAL_MS:MAX_INTERVAL_MS,BACKOFF_FACTOR:BACKOFF_FACTOR,MAX_BACKOFF_MS:MAX_BACKOFF_MS,REQUEST_TIMEOUT_MS:REQUEST_TIMEOUT_MS,APP_NAMES:APP_NAMES,saneToken:saneToken,saneCount:saneCount}=require("./presence-constants.js"),DEFAULT_API_BASE="https://aegiscloud.org";function disabledPresence(e){const n={enabled:!1,reason:String(e||"disabled"),beats:0,failures:0,lastError:null,lastSeenAt:null,intervalMs:null,install:null,app:null,running:!1};return{enabled:!1,reason:n.reason,install:null,beat:()=>Promise.resolve(!1),start:()=>!1,stop:()=>!1,state:()=>({...n})}}function isoOf(e){try{const n=Number(e);return Number.isFinite(n)?new Date(n).toISOString():null}catch{return null}}function createPresence(e={}){const n="string"==typeof e.apiKey?e.apiKey.trim():"";if(!n)return disabledPresence("no-api-key");if(!1===e.enabled)return disabledPresence("disabled");const t="function"==typeof e.log?e.log:()=>{};function r(e){if(!o.has(e)){o.add(e);try{t("debug",`presence: ${e}`)}catch{}}}const o=new Set,s="function"==typeof e.now?e.now:()=>Date.now(),i="function"==typeof e.setTimer?e.setTimer:setTimeout,a="function"==typeof e.clearTimer?e.clearTimer:clearTimeout;let l=saneToken(e.install,64);if(l||"function"!=typeof e.fingerprint){if(!l)try{l=saneToken(require("./device-fingerprint.js").machineFingerprint(),64)}catch{r("fingerprint")}}else try{l=saneToken(e.fingerprint(),64)}catch{r("fingerprint")}if(!l)return disabledPresence("no-install-identity");const u=saneToken(e.app,24).toLowerCase()||"other",c=saneToken(e.version,32),f=saneToken(e.platform,16).toLowerCase()||("undefined"!=typeof process&&process.platform?process.platform:""),_=String(e.baseUrl||"undefined"!=typeof process&&process.env&&process.env.AEGIS_API_BASE||DEFAULT_API_BASE).replace(/\/+$/,""),A=PRESENCE_PATH,E="function"==typeof e.fetchImpl?e.fetchImpl:"function"==typeof fetch?fetch:null;function M(e,n){try{return"function"==typeof e?e():e}catch{return n}}let p=b(e.intervalMs);const T={enabled:!0,reason:null,beats:0,failures:0,lastError:null,lastSeenAt:null,intervalMs:p,install:l,app:u,version:c||null,platform:f||null,running:!1};let S=null,m=null,d=0,N=!1;function b(e){const n=Number(e);return!Number.isFinite(n)||n<=0?DEFAULT_INTERVAL_MS:Math.min(MAX_INTERVAL_MS,Math.max(MIN_INTERVAL_MS,Math.floor(n)))}function y(){if(N)return Promise.resolve(!1);if(m)return m;if(!E)return r("no-fetch"),T.lastError="no fetch implementation",T.failures+=1,Promise.resolve(!1);const t=s();return m=(async()=>{let o=!1,P=null;try{const t="function"==typeof AbortController?new AbortController:null;if(t){const e=i((()=>{o=!0;try{t.abort()}catch{}}),REQUEST_TIMEOUT_MS);e&&"function"==typeof e.unref&&e.unref(),P=()=>a(e)}const r=await E(`${_}${A}`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":n,"X-AEGIS-Version":saneToken(e.clientVersion,32)||c||""},body:JSON.stringify({app:u,version:c||void 0,platform:f||void 0,install:l,queue:saneCount(M(e.queueDepth,0)),autonomous:Boolean(M(e.autonomous,!1))}),...t?{signal:t.signal}:{}});if(P&&P(),!r||"number"!=typeof r.status||r.status<200||r.status>=300)return T.failures+=1,T.lastError=`http ${r&&r.status?r.status:"error"}`,d=I(d),!1;let S=null;try{S=await r.json()}catch{S=null}if(T.beats+=1,T.failures=0,T.lastError=null,T.lastSeenAt=isoOf(s()),d=0,S&&"object"==typeof S&&null!=S.beat_interval_s){const e=Number(S.beat_interval_s);Number.isFinite(e)&&e>0&&(p=b(1e3*e))}return T.intervalMs=p,!0}catch(e){return P&&P(),T.failures+=1,T.lastError=o?"timeout":String(e&&e.message||e||"error").slice(0,200),d=I(d),1===T.failures&&r("beat-failed"),!1}finally{m=null,T.running&&function(e){if(N||"function"!=typeof i)return;S&&a(S);const n=Math.max(1e3,Math.floor(Number(e)||p));S=i((()=>{S=null,y()}),n),S&&"function"==typeof S.unref&&S.unref()}(Math.max(0,p+d-(s()-t)))}})(),m}function I(e){const n=e>0?e*BACKOFF_FACTOR:Math.max(MIN_INTERVAL_MS,p);return Math.min(MAX_BACKOFF_MS,Math.floor(n))}return{enabled:!0,app:u,install:l,beat:y,start:function(){return!N&&(T.running=!0,y(),!0)},stop:function(){if(N=!0,T.running=!1,S){try{a(S)}catch{}S=null}return!0},pending:()=>m,state:()=>({...T,intervalMs:p})}}module.exports={createPresence:createPresence,disabledPresence:disabledPresence,PRESENCE_PATH:PRESENCE_PATH,DEFAULT_INTERVAL_MS:DEFAULT_INTERVAL_MS,MIN_INTERVAL_MS:MIN_INTERVAL_MS,MAX_INTERVAL_MS:MAX_INTERVAL_MS,MAX_BACKOFF_MS:MAX_BACKOFF_MS,REQUEST_TIMEOUT_MS:REQUEST_TIMEOUT_MS,APP_NAMES:APP_NAMES};