aegis-desktop 1.0.2 → 1.0.4

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,58 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ 'use strict';
4
+
5
+ /**
6
+ * The desktop's view of whether this machine has an AEGIS account key. When
7
+ * startup could not provision one, the banner says why and offers a retry.
8
+ * Pure: Electron, the store and the provisioner are injected.
9
+ */
10
+
11
+ function provisionFailureText(result) {
12
+ const err = result && result.error;
13
+ if (!err) return 'the server returned no account key';
14
+ if (err.status === 429) return 'too many sign-ups from this network — try again later';
15
+ if (!err.status || err.status === 408 || err.status >= 500) return 'could not reach aegiscloud.org';
16
+ return `the server refused the request (HTTP ${err.status})`;
17
+ }
18
+
19
+ function createAccountKey({ aegis, credentials, persistKey, provision, onChange }) {
20
+ let failure = null;
21
+ let inFlight = null;
22
+
23
+ function status() {
24
+ if (aegis.apiKey || !failure) return null;
25
+ return { status: 'no-key', reason: failure };
26
+ }
27
+
28
+ function recordStartup(result) {
29
+ failure = (result.key || aegis.apiKey) ? null : provisionFailureText(result);
30
+ onChange(status());
31
+ }
32
+
33
+ function registerAccount() {
34
+ if (!inFlight) {
35
+ inFlight = (async () => {
36
+ try {
37
+ const result = await provision(aegis, credentials);
38
+ if (result.key) {
39
+ persistKey(result.key);
40
+ failure = null;
41
+ } else {
42
+ failure = provisionFailureText(result);
43
+ }
44
+ const state = status();
45
+ onChange(state);
46
+ return state;
47
+ } finally {
48
+ inFlight = null;
49
+ }
50
+ })();
51
+ }
52
+ return inFlight;
53
+ }
54
+
55
+ return { status, recordStartup, registerAccount };
56
+ }
57
+
58
+ module.exports = { createAccountKey, provisionFailureText };
package/main.js CHANGED
@@ -152,6 +152,7 @@ try {
152
152
  // backed by transport-only modules in desktop/lib/. No brain logic enters here.
153
153
  const os = require('node:os');
154
154
  const { createLocalEngine } = require('./lib/local/engine.js');
155
+ const { createAccountKey } = require('./lib/account-key.js');
155
156
  const { createSettingsStore, isReservedNamespace } = require('./lib/settings.js');
156
157
  const sessionStore = require('./lib/sync/sessions.js');
157
158
  const memoryQueue = require('./lib/sync/memory-queue.js');
@@ -2633,6 +2634,7 @@ function registerAutoIpc(ipcMain, dispatch) {
2633
2634
  * polling `aegis:updateStatus`.
2634
2635
  */
2635
2636
  const UPDATE_STATUS_CHANNEL = `${IPC_PREFIX}updateStatus`;
2637
+ const ACCOUNT_STATUS_CHANNEL = `${IPC_PREFIX}accountStatusChanged`;
2636
2638
 
2637
2639
  /** Re-check for a new release every few hours — frequent enough that a
2638
2640
  * long-lived session still notices a release, rare enough to not hammer
@@ -3203,6 +3205,29 @@ function buildAppMenu({ app, Menu, BrowserWindow, updateManager }) {
3203
3205
  function bootstrap() {
3204
3206
  const { app, BrowserWindow, ipcMain, safeStorage, shell, Menu, Notification, screen, dialog, globalShortcut } = electron;
3205
3207
 
3208
+ // A container/CI/headless server with no usable GPU crashes Chromium's GPU
3209
+ // process in a retry loop that ends in a FATAL exit ("GPU process isn't
3210
+ // usable. Goodbye.") — and that happens BEFORE app.whenReady() ever fires,
3211
+ // so autoProvisionApiKey() (the free device key + welcome-trial grant)
3212
+ // never runs either: `npm install -g aegis-desktop && aegis` on exactly
3213
+ // that class of machine registered nothing, with no error a user could
3214
+ // act on. Must be called before the app is ready (Electron's own
3215
+ // constraint); software rendering is a fine tradeoff for a text/chat UI
3216
+ // with no WebGL or video.
3217
+ //
3218
+ // disableHardwareAcceleration() alone was not enough on a container whose
3219
+ // Mesa/VA-API/DRI stack is fully broken rather than merely GPU-less:
3220
+ // Chromium still spawned a GPU process for the SwiftShader software path,
3221
+ // and that crash-looped to the same FATAL exit, just after a longer delay
3222
+ // (measured: survived past 15s, died at ~44s). These three switches tell
3223
+ // Chromium to skip GPU-process creation entirely rather than retry it
3224
+ // under software rendering — the documented combination for Electron under
3225
+ // Docker/CI/headless Linux.
3226
+ app.disableHardwareAcceleration();
3227
+ app.commandLine.appendSwitch('disable-gpu');
3228
+ app.commandLine.appendSwitch('disable-software-rasterizer');
3229
+ app.commandLine.appendSwitch('disable-gpu-compositing');
3230
+
3206
3231
  // A second launch (double-clicking the icon again, `aegis` from a second
3207
3232
  // terminal, …) must focus the existing window, not spawn a second process
3208
3233
  // fighting the first over the same on-disk settings/session/queue files —
@@ -3272,6 +3297,53 @@ function bootstrap() {
3272
3297
  dir: dataDir,
3273
3298
  });
3274
3299
 
3300
+ // Resolve/auto-provision the account key as early as possible — BEFORE
3301
+ // app.whenReady(), not inside it. autoProvisionApiKey() is a plain Node
3302
+ // fetch call with no Electron dependency: createClient(credentials.
3303
+ // clientOptions()) above already resolved any existing key (env, the
3304
+ // shared file, the env-file fallback) onto `aegis.apiKey`, so this is a
3305
+ // genuine registerDevice call only on a truly fresh install — the same
3306
+ // one-call-is-enough guarantee autoProvisionApiKey() provides to the CLI
3307
+ // and the MCP plugin. It persists straight to the plain shared
3308
+ // credentials file via credentials.saveApiKey(), not safeStorage.
3309
+ //
3310
+ // Why this can't wait for 'ready': a container/CI host with a broken
3311
+ // graphics stack crashes Chromium's GPU process in a retry loop that can
3312
+ // end in a FATAL exit (see disableHardwareAcceleration above) BEFORE
3313
+ // whenReady() ever resolves — measured here surviving 15s before dying at
3314
+ // 42-44s even with hardware acceleration and GPU compositing disabled.
3315
+ // Gating registration on 'ready' meant that exact class of host — exactly
3316
+ // what `npm install -g aegis-desktop && aegis` on a headless server lands
3317
+ // on — registered nothing and left no trace. Running it here means the
3318
+ // account exists and the key is on disk the moment the process starts,
3319
+ // independent of whether the window ever manages to open.
3320
+ //
3321
+ // Not calling settings.setAegisKey() here: safeStorage is not safe to use
3322
+ // before 'ready' (see migrateLegacyAegisKey's own comment in desktop/lib/
3323
+ // settings.js, the one other place in this codebase that already had to
3324
+ // reason about this). The
3325
+ // existing app.whenReady() block further down already re-reads the shared
3326
+ // store via credentials.preferNewest() and writes the encrypted copy once
3327
+ // ready — this just guarantees there is something there for it to find
3328
+ // even when 'ready' never comes.
3329
+ const accountKey = createAccountKey({
3330
+ aegis,
3331
+ credentials,
3332
+ persistKey: (key) => settings.setAegisKey(key),
3333
+ provision: autoProvision.autoProvisionApiKey,
3334
+ onChange: (state) => broadcastAccountStatus(state),
3335
+ });
3336
+ const earlyKeyProvision = autoProvision.autoProvisionApiKey(aegis, credentials).then((result) => {
3337
+ accountKey.recordStartup(result);
3338
+ if (!result.key && !aegis.apiKey) {
3339
+ const reason =
3340
+ (result.error && (result.error.status || result.error.message || String(result.error))) ||
3341
+ 'no key returned';
3342
+ console.warn(`auto-provision: no account key from register-device (${reason})`);
3343
+ }
3344
+ return result;
3345
+ });
3346
+
3275
3347
  // A provider key typed in the Settings pane used to reach ONLY this app's
3276
3348
  // encrypted store, so `aegiscode` (the CLI) and the MCP plugin never saw it:
3277
3349
  // same machine, same provider, key asked for twice, and a key entered here
@@ -3706,6 +3778,11 @@ function bootstrap() {
3706
3778
  if (!win.isDestroyed()) win.webContents.send(UPDATE_STATUS_CHANNEL, state);
3707
3779
  }
3708
3780
  }
3781
+ function broadcastAccountStatus(state) {
3782
+ for (const win of openWindows) {
3783
+ if (!win.isDestroyed()) win.webContents.send(ACCOUNT_STATUS_CHANNEL, state);
3784
+ }
3785
+ }
3709
3786
 
3710
3787
  const updateManager = createUpdateManager({
3711
3788
  autoUpdater: realAutoUpdater,
@@ -3713,6 +3790,8 @@ function bootstrap() {
3713
3790
  onStatus: broadcastUpdateStatus,
3714
3791
  });
3715
3792
  registerUpdateIpc(ipcMain, updateManager);
3793
+ ipcMain.handle(`${IPC_PREFIX}accountStatus`, () => accountKey.status());
3794
+ ipcMain.handle(`${IPC_PREFIX}registerAccount`, () => accountKey.registerAccount());
3716
3795
  registerExportIpc(
3717
3796
  ipcMain,
3718
3797
  sessionStore,
@@ -3820,41 +3899,24 @@ function bootstrap() {
3820
3899
  // it, and nothing here may shadow an explicit export.
3821
3900
  const envKey = credentials.normalizeApiKey(process.env[credentials.KEY_ENV]);
3822
3901
  if (!envKey) {
3902
+ // Wait for the pre-ready attempt above rather than racing a second
3903
+ // registerDevice call: autoProvisionApiKey() already ran (or is still
3904
+ // running) the instant this process started, specifically so it is
3905
+ // not gated on 'ready' — see the earlyKeyProvision comment above for
3906
+ // why (a container with a broken graphics stack can crash before
3907
+ // 'ready' ever fires). Swallowing a keyless outcome here was how a
3908
+ // machine could sit absent from the admin panel's user list AND its
3909
+ // presence card with no trace anywhere — the server now logs every
3910
+ // registration attempt, and earlyKeyProvision's own .then() already
3911
+ // warns on failure, so there is nothing further to report if it
3912
+ // didn't land.
3913
+ await earlyKeyProvision;
3823
3914
  const chosen = credentials.preferNewest(settings.aegisRawKey(), settings.aegisKeySavedAt());
3824
3915
  if (chosen.key) {
3825
3916
  aegis.setApiKey(chosen.key);
3826
3917
  // The shared store is the newer write: bring this host's encrypted copy
3827
3918
  // up to date so a CLI rotation does not have to be redone in Settings.
3828
3919
  if (chosen.source === 'shared') settings.setAegisKey(chosen.key);
3829
- } else {
3830
- // Nothing configured anywhere (env, this app's encrypted copy, or the
3831
- // shared store): a fresh install. Auto-provision a free device key
3832
- // from aegis1 — same shared helper mcp/server.js and the CLI use, so
3833
- // the desktop app works with no manual sign-in step. A brand-new
3834
- // fingerprint is granted aegis1's 30-day welcome-trial `unlimited`
3835
- // entitlement (token-bank credit + a subscriptions row) in the same
3836
- // register-device call that mints the key — see client/
3837
- // auto-provision.js's header for the aegis1 commit that made this a
3838
- // real grant rather than a badge. Failure (offline, aegis1
3839
- // unreachable) falls through exactly as before: the renderer's
3840
- // normal "not signed in" state handles it, and the next launch tries
3841
- // again.
3842
- const result = await autoProvision.autoProvisionApiKey(aegis, credentials);
3843
- if (result.key) settings.setAegisKey(result.key);
3844
- else {
3845
- // Swallowing this was how a machine could sit keyless — and therefore
3846
- // absent from the admin panel's user list AND its presence card —
3847
- // with no trace anywhere: the app just opened in its normal
3848
- // signed-out state, and the next launch retried with the same
3849
- // invisible outcome. The server now logs every registration attempt;
3850
- // this closes the other half so one launch is diagnosable. A 429 here
3851
- // is not fatal (aegis1 creates the account and withholds the trial),
3852
- // so it must read as "retry", not as a broken install.
3853
- const reason =
3854
- (result.error && (result.error.status || result.error.message || String(result.error))) ||
3855
- 'no key returned';
3856
- console.warn(`auto-provision: no account key from register-device (${reason})`);
3857
- }
3858
3920
  }
3859
3921
  }
3860
3922
  // ── presence beacon ───────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aegis-desktop",
3
3
  "productName": "AEGIS Desktop",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
6
6
  "author": {
7
7
  "name": "AEGIS Code",
@@ -32,7 +32,7 @@
32
32
  "demo": "node scripts/record-demo-gif.mjs",
33
33
  "dist": "npm run predist && electron-builder",
34
34
  "dist:dir": "npm run predist && electron-builder --dir",
35
- "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check scripts/bundle-fonts.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/local/queue.js && node --check lib/local/autonomous.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
35
+ "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check scripts/bundle-fonts.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/local/queue.js && node --check lib/local/autonomous.js && node --check lib/account-key.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
36
36
  "test:shell": "node ../test/desktop-shell.mjs",
37
37
  "test:model": "node ../test/model-dispatch.mjs",
38
38
  "test:budget": "node ../test/budget.test.mjs",
@@ -55,7 +55,8 @@
55
55
  "test:avatar-events": "node test/avatar-events.test.mjs",
56
56
  "test:avatar-tiers": "node test/avatar-tiers.test.mjs",
57
57
  "test:avatar-persona": "node test/avatar-persona.test.mjs",
58
- "test:renderer-fonts": "node test/renderer-fonts.test.mjs"
58
+ "test:renderer-fonts": "node test/renderer-fonts.test.mjs",
59
+ "test:account-key": "node test/account-key.test.mjs"
59
60
  },
60
61
  "dependencies": {
61
62
  "electron-updater": "^6.8.9"
package/preload.js CHANGED
@@ -27,6 +27,7 @@ const AUTO_PREFIX = 'auto:';
27
27
  const QUEUE_PROGRESS_CHANNEL = `${QUEUE_PREFIX}progress`;
28
28
  const CHAT_DELTA_CHANNEL = `${IPC_PREFIX}chatDelta`;
29
29
  const UPDATE_STATUS_CHANNEL = `${IPC_PREFIX}updateStatus`;
30
+ const ACCOUNT_STATUS_CHANNEL = `${IPC_PREFIX}accountStatusChanged`;
30
31
  const MENU_NEW_CHAT_CHANNEL = `${IPC_PREFIX}menuNewChat`;
31
32
  const MENU_SEARCH_CHANNEL = `${IPC_PREFIX}menuSearch`;
32
33
  const MENU_EXPORT_MARKDOWN_CHANNEL = `${IPC_PREFIX}menuExportMarkdown`;
@@ -268,6 +269,13 @@ const api = {
268
269
  ipcRenderer.on(UPDATE_STATUS_CHANNEL, listener);
269
270
  return () => ipcRenderer.removeListener(UPDATE_STATUS_CHANNEL, listener);
270
271
  },
272
+ accountStatus: () => invoke('accountStatus'),
273
+ registerAccount: () => invoke('registerAccount'),
274
+ onAccountStatus: (onStatus) => {
275
+ const listener = (_event, state) => onStatus(state);
276
+ ipcRenderer.on(ACCOUNT_STATUS_CHANNEL, listener);
277
+ return () => ipcRenderer.removeListener(ACCOUNT_STATUS_CHANNEL, listener);
278
+ },
271
279
  // Native menu accelerators with no built-in Electron role (Cmd/Ctrl+N,
272
280
  // Cmd/Ctrl+K — see main.js buildAppMenu): main.js just pings the channel,
273
281
  // the renderer owns what "new chat" / "search" actually do.
package/renderer/app.js CHANGED
@@ -73,6 +73,9 @@ const ELEMENT_IDS = {
73
73
  updateRestartBtn: 'update-restart-btn',
74
74
  updateRetryBtn: 'update-retry-btn',
75
75
  updateLaterBtn: 'update-later-btn',
76
+ accountBanner: 'account-banner',
77
+ accountBannerText: 'account-banner-text',
78
+ accountRetryBtn: 'account-retry-btn',
76
79
  connDot: 'conn-dot',
77
80
  connText: 'conn-text',
78
81
  app: 'st-app',
@@ -1140,6 +1143,17 @@ function renderUpdateBanner(state) {
1140
1143
  els.updateBanner.hidden = false;
1141
1144
  }
1142
1145
 
1146
+ function renderAccountBanner(state) {
1147
+ if (!els.accountBanner) return;
1148
+ if (!state || state.status !== 'no-key') {
1149
+ els.accountBanner.hidden = true;
1150
+ return;
1151
+ }
1152
+ els.accountBannerText.textContent =
1153
+ `No AEGIS account key on this machine: ${state.reason}.`;
1154
+ els.accountBanner.hidden = false;
1155
+ }
1156
+
1143
1157
  async function loadAccountInfo() {
1144
1158
  try {
1145
1159
  const verify = await aegis.verifyApiKey();
@@ -5703,6 +5717,20 @@ async function init() {
5703
5717
 
5704
5718
  // Auto-update banner: `?`-guarded like the memory inspector above, since
5705
5719
  // the markup is optional and a missing node must never crash boot.
5720
+ if (els.accountBanner && aegis.onAccountStatus) {
5721
+ aegis.onAccountStatus(renderAccountBanner);
5722
+ aegis.accountStatus().then(renderAccountBanner).catch((err) => console.warn('account status unavailable', err));
5723
+ els.accountRetryBtn.addEventListener('click', () => {
5724
+ els.accountRetryBtn.disabled = true;
5725
+ aegis.registerAccount()
5726
+ .then(renderAccountBanner)
5727
+ .catch((err) => renderAccountBanner({ status: 'no-key', reason: String((err && err.message) || err) }))
5728
+ .finally(() => {
5729
+ els.accountRetryBtn.disabled = false;
5730
+ });
5731
+ });
5732
+ }
5733
+
5706
5734
  if (els.updateBanner && aegis.onUpdateStatus) {
5707
5735
  aegis.onUpdateStatus(renderUpdateBanner);
5708
5736
  aegis.updateStatus().then(renderUpdateBanner).catch(() => {});
@@ -48,6 +48,13 @@
48
48
  </div>
49
49
  </div>
50
50
 
51
+ <div class="update-banner account-banner" id="account-banner" role="alert" hidden>
52
+ <span id="account-banner-text"></span>
53
+ <div class="update-banner-actions">
54
+ <button type="button" id="account-retry-btn" class="ghost-btn">Retry account setup</button>
55
+ </div>
56
+ </div>
57
+
51
58
  <main>
52
59
  <aside class="side">
53
60
  <section class="card">
@@ -33,32 +33,48 @@
33
33
  const { machineFingerprint } = require('./device-fingerprint.js');
34
34
 
35
35
  const DEVICE_REGISTER_TIMEOUT_MS = 8000;
36
+ const DEVICE_REGISTER_RETRY_DELAYS_MS = [1000, 3000];
37
+
38
+ // Network failure, timeout (status 0), 408, or 5xx. A 4xx — including 429, the
39
+ // server's per-IP registration cap — is a refusal, and retrying it only spends
40
+ // more of that cap.
41
+ function isTransientRegisterError(err) {
42
+ const status = err && err.status;
43
+ return !status || status === 408 || status >= 500;
44
+ }
36
45
 
37
46
  /**
38
47
  * @param {object} aegisClient A client from client/aegis.js (createClient()).
39
48
  * @param {object} credentials client/credentials.js (or a compatible double).
49
+ * @param {number[]} [retryDelaysMs] Wait before each retry of a transient failure.
40
50
  * @returns {Promise<{ok: boolean, key: string, error?: Error}>}
41
51
  * `ok` is true only when a usable key was obtained AND persisted to the
42
52
  * shared store; `aegisClient.setApiKey()` has already been called on
43
53
  * success, so the caller's client is immediately usable either way it
44
54
  * inspects `ok`.
45
55
  */
46
- async function autoProvisionApiKey(aegisClient, credentials) {
56
+ async function autoProvisionApiKey(aegisClient, credentials, retryDelaysMs = DEVICE_REGISTER_RETRY_DELAYS_MS) {
47
57
  if (aegisClient.apiKey) return { ok: true, key: aegisClient.apiKey };
48
- const controller = new AbortController();
49
- const timer = setTimeout(() => controller.abort(), DEVICE_REGISTER_TIMEOUT_MS);
50
- try {
51
- const data = await aegisClient.registerDevice(machineFingerprint(), { signal: controller.signal });
52
- const key = data && typeof data.api_key === 'string' ? data.api_key.trim() : '';
53
- if (!key) return { ok: false, key: '' };
54
- const saved = credentials.saveApiKey(key);
55
- aegisClient.setApiKey(key);
56
- return { ok: !!saved.ok, key };
57
- } catch (err) {
58
- return { ok: false, key: '', error: err };
59
- } finally {
60
- clearTimeout(timer);
58
+ let lastError = null;
59
+ for (let attempt = 0; attempt <= retryDelaysMs.length; attempt++) {
60
+ if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt - 1]));
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), DEVICE_REGISTER_TIMEOUT_MS);
63
+ try {
64
+ const data = await aegisClient.registerDevice(machineFingerprint(), { signal: controller.signal });
65
+ const key = data && typeof data.api_key === 'string' ? data.api_key.trim() : '';
66
+ if (!key) return { ok: false, key: '' };
67
+ const saved = credentials.saveApiKey(key);
68
+ aegisClient.setApiKey(key);
69
+ return { ok: !!saved.ok, key };
70
+ } catch (err) {
71
+ lastError = err;
72
+ if (!isTransientRegisterError(err)) break;
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
61
76
  }
77
+ return { ok: false, key: '', error: lastError };
62
78
  }
63
79
 
64
- module.exports = { autoProvisionApiKey, DEVICE_REGISTER_TIMEOUT_MS };
80
+ module.exports = { autoProvisionApiKey, DEVICE_REGISTER_TIMEOUT_MS, DEVICE_REGISTER_RETRY_DELAYS_MS };