tabby-rdp 0.5.0 → 0.5.2

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 (87) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +216 -58
  3. package/TROUBLESHOOTING.md +77 -0
  4. package/dist/accountForm.js +0 -1
  5. package/dist/accountRevisions.js +46 -0
  6. package/dist/accounts.js +2 -2
  7. package/dist/audio.js +0 -1
  8. package/dist/authorities.js +331 -0
  9. package/dist/byteQueue.js +102 -0
  10. package/dist/clipboard.js +106 -0
  11. package/dist/connectionStatus.js +0 -1
  12. package/dist/desk.js +16 -47
  13. package/dist/deskScript.js +196 -6
  14. package/dist/desktop.service.js +1499 -269
  15. package/dist/desktopForm.js +53 -18
  16. package/dist/desktops.js +232 -24
  17. package/dist/diskStorage.js +212 -0
  18. package/dist/drives.js +271 -30
  19. package/dist/fileTransfer.js +960 -132
  20. package/dist/gateway.js +216 -57
  21. package/dist/header.js +76 -5
  22. package/dist/help.js +16 -47
  23. package/dist/hostCompat.js +86 -0
  24. package/dist/hyperv.js +65 -15
  25. package/dist/index.js +7 -4
  26. package/dist/keyboard.js +0 -1
  27. package/dist/microphone.js +57 -13
  28. package/dist/ntlm.js +0 -1
  29. package/dist/osd.js +56 -15
  30. package/dist/packageChangeDialog.js +186 -0
  31. package/dist/probe.js +15 -3
  32. package/dist/rdcleanpath.js +319 -44
  33. package/dist/rdpFile.js +22 -7
  34. package/dist/rdpProfile.js +39 -21
  35. package/dist/releaseSettings.js +72 -0
  36. package/dist/releases.js +408 -0
  37. package/dist/remoteSetup.js +103 -23
  38. package/dist/sessionLog.js +53 -0
  39. package/dist/settingsPage.js +148 -122
  40. package/dist/signin.js +478 -48
  41. package/dist/ssh.js +99 -18
  42. package/dist/sshChannelStream.js +40 -6
  43. package/dist/targets.js +194 -40
  44. package/dist/ui.js +88 -20
  45. package/dist/unshowable.js +41 -0
  46. package/dist/updates.js +79 -56
  47. package/dist/vms.js +17 -3
  48. package/dist/wake.js +15 -8
  49. package/docs/desk.md +157 -0
  50. package/package.json +14 -4
  51. package/remote/trd-pty.py +74 -13
  52. package/vendor/SHA256SUMS +26 -0
  53. package/vendor/ironrdp_web_bg.wasm +0 -0
  54. package/dist/accountForm.js.map +0 -1
  55. package/dist/accounts.js.map +0 -1
  56. package/dist/audio.js.map +0 -1
  57. package/dist/connectionStatus.js.map +0 -1
  58. package/dist/desk.js.map +0 -1
  59. package/dist/deskScript.js.map +0 -1
  60. package/dist/desktop.service.js.map +0 -1
  61. package/dist/desktopForm.js.map +0 -1
  62. package/dist/desktops.js.map +0 -1
  63. package/dist/drives.js.map +0 -1
  64. package/dist/fileTransfer.js.map +0 -1
  65. package/dist/gateway.js.map +0 -1
  66. package/dist/header.js.map +0 -1
  67. package/dist/help.js.map +0 -1
  68. package/dist/hyperv.js.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/keyboard.js.map +0 -1
  71. package/dist/microphone.js.map +0 -1
  72. package/dist/ntlm.js.map +0 -1
  73. package/dist/osd.js.map +0 -1
  74. package/dist/probe.js.map +0 -1
  75. package/dist/rdcleanpath.js.map +0 -1
  76. package/dist/rdpFile.js.map +0 -1
  77. package/dist/rdpProfile.js.map +0 -1
  78. package/dist/remoteSetup.js.map +0 -1
  79. package/dist/settingsPage.js.map +0 -1
  80. package/dist/signin.js.map +0 -1
  81. package/dist/ssh.js.map +0 -1
  82. package/dist/sshChannelStream.js.map +0 -1
  83. package/dist/targets.js.map +0 -1
  84. package/dist/ui.js.map +0 -1
  85. package/dist/updates.js.map +0 -1
  86. package/dist/vms.js.map +0 -1
  87. package/dist/wake.js.map +0 -1
@@ -18,9 +18,10 @@ const desktopForm_1 = require("./desktopForm");
18
18
  const help_1 = require("./help");
19
19
  const accounts_1 = require("./accounts");
20
20
  const accountForm_1 = require("./accountForm");
21
+ const clipboard_1 = require("./clipboard");
21
22
  const desktops_1 = require("./desktops");
22
23
  const gateway_1 = require("./gateway");
23
- const signin_1 = require("./signin");
24
+ const osd_1 = require("./osd");
24
25
  const ssh_1 = require("./ssh");
25
26
  const targets_1 = require("./targets");
26
27
  /** Input of an SSH tab opened for an RDP profile with `via`: the desktop to show once SSH is connected. */
@@ -134,7 +135,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
134
135
  const o = this.profile.options;
135
136
  const account = () => `${o.username ?? ''}\n${o.domain ?? ''}\n${o.account ?? ''}`;
136
137
  // No id for a new profile (no address yet): it has nothing saved to take along.
137
- this.before = { id: o.host ? (0, desktops_1.desktopIdOf)(o) : '', account: account(), via: o.via ?? '' };
138
+ this.before = { id: o.host ? (0, desktops_1.desktopIdOf)(o) : '', account: account(), via: o.via ?? '', gateway: o.gateway ?? '' };
138
139
  const root = this.element.nativeElement;
139
140
  root.innerHTML = `
140
141
  <div class="mb-3">
@@ -179,6 +180,12 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
179
180
  <label>Gateway account</label>
180
181
  <select class="form-control" name="gatewayAccount"></select>
181
182
  <div class="text-muted small">A gateway that takes another account than the desktop: a saved account for it.</div>
183
+ </div>
184
+ <div class="mb-3">
185
+ <label>Clipboard</label>
186
+ <select class="form-control" name="clipboard"></select>
187
+ <div class="text-muted small">Which ways text, pictures and files copied on either side go between this computer and the desktop.
188
+ Applies on the next connection.</div>
182
189
  </div>`;
183
190
  const field = (name) => root.querySelector(`[name="${name}"]`);
184
191
  field('address').value = o.host ? (0, desktopForm_1.formatAddress)(o.host, o.port || 3389) : '';
@@ -240,6 +247,11 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
240
247
  });
241
248
  gatewayAccount.addEventListener('change', () => { o.gatewayAccount = gatewayAccount.value; });
242
249
  gatewayChanged();
250
+ // Its own clipboard sharing, or the setting's (named, as it is now).
251
+ const clipboard = field('clipboard');
252
+ clipboard.append(new Option(`As in Settings › Remote Desktop (${clipboard_1.CLIPBOARD_LABELS[desktop.settings().clipboard].toLowerCase()})`, ''), ...Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, text]) => new Option(text, mode)));
253
+ clipboard.value = (0, clipboard_1.ownClipboard)(o.clipboard) ?? '';
254
+ clipboard.addEventListener('change', () => { o.clipboard = clipboard.value; });
243
255
  const via = field('via');
244
256
  via.addEventListener('change', () => { o.via = via.value; });
245
257
  this.injector.get(tabby_core_1.ProfilesService).getProfiles().then(profiles => {
@@ -253,21 +265,22 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
253
265
  }
254
266
  save() {
255
267
  const o = this.profile.options;
256
- const { id, account, via } = this.before;
268
+ const { id, account, via, gateway } = this.before;
257
269
  if (!id) {
258
270
  return;
259
271
  }
260
272
  if ((o.via ?? '') !== via) {
261
- // Reached another way, it is another desktop. A direct one's saved account and certificate were its own; one
262
- // behind a host shares them with that host's desktop at the same address, so they stay.
273
+ // Reached another way, it is another desktop. A direct one's saved passwords (through any gateway, the ones
274
+ // it had before included) and certificate were its own; one behind a host shares them with that host's
275
+ // desktop at the same address, so they stay.
263
276
  if (!via) {
264
- (0, signin_1.forgetCredentials)(`${desktops_1.DIRECT_KEY}#${id}`);
265
- this.injector.get(desktop_service_1.RemoteDesktopService).forgetCertificatesFor(id, true);
277
+ this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop(id, true, `"${this.profile.name}"`).catch(() => null);
266
278
  }
267
279
  return;
268
280
  }
281
+ // Its saved password goes with its way there (see desktopEdited): another gateway is asked for again.
269
282
  const accountChanged = `${o.username ?? ''}\n${o.domain ?? ''}\n${o.account ?? ''}` !== account;
270
- this.injector.get(desktop_service_1.RemoteDesktopService).desktopEdited(id, (0, desktops_1.desktopIdOf)(o), accountChanged, !via).catch(() => null);
283
+ this.injector.get(desktop_service_1.RemoteDesktopService).desktopEdited(id, (0, desktops_1.desktopIdOf)(o), accountChanged, !via, { from: gateway, to: o.gateway }).catch(() => null);
271
284
  }
272
285
  };
273
286
  exports.RDPProfileSettingsComponent = RDPProfileSettingsComponent;
@@ -289,11 +302,14 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
289
302
  this.name = 'Remote desktop (RDP)';
290
303
  this.settingsComponent = RDPProfileSettingsComponent;
291
304
  this.configDefaults = {
292
- options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '', gateway: '', gatewayAccount: '' },
305
+ options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '', gateway: '', gatewayAccount: '', clipboard: '' },
293
306
  clearServiceMessagesOnConnect: false,
294
307
  };
295
308
  }
296
309
  async getBuiltinProfiles() {
310
+ // No clipboard here: a new profile is made from a copy of this, and a value saved with it (even '', the
311
+ // setting's) would hide the clipboard its group's or the type's defaults give it, which may well be narrower.
312
+ // Left out, the profile follows those defaults (configDefaults has the key) until it is given one of its own.
297
313
  return [{
298
314
  id: `${targets_1.RDP_PROFILE_TYPE}:template`,
299
315
  type: targets_1.RDP_PROFILE_TYPE,
@@ -323,13 +339,17 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
323
339
  }
324
340
  getDescription(profile) {
325
341
  const o = profile.options ?? {};
326
- if (!o.host) {
342
+ // Each field as text whatever the config holds (see configText): Tabby asks this of every profile as it lists
343
+ // them in its profile selector, and one that throws keeps the selector from opening at all.
344
+ const host = (0, desktops_1.configText)(o.host);
345
+ if (!host) {
327
346
  return '';
328
347
  }
329
- const address = (0, desktopForm_1.formatAddress)(o.host, o.port || 3389);
348
+ const address = (0, desktopForm_1.formatAddress)(host, (0, osd_1.configNumber)(o.port || 3389));
349
+ const gateway = (0, desktops_1.configText)(o.gateway);
330
350
  const via = [
331
- ...o.gateway ? [`the gateway ${o.gateway}`] : [],
332
- ...o.via ? [(this.config.store.profiles ?? []).find((p) => p.id === o.via)?.name ?? 'an SSH profile'] : [],
351
+ ...gateway ? [`the gateway ${gateway}`] : [],
352
+ ...o.via ? [(0, desktops_1.configText)((this.config.store.profiles ?? []).find((p) => p.id === o.via)?.name) || 'an SSH profile'] : [],
333
353
  ];
334
354
  return via.length ? `${address} via ${via.join(', via ')}` : address;
335
355
  }
@@ -348,16 +368,15 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
348
368
  }
349
369
  intoQuickConnectString(profile) {
350
370
  const o = profile.options;
351
- return o.via ? null : `${o.username ? `${o.username}@` : ''}${(0, desktopForm_1.formatAddress)(o.host, o.port || 3389)}`;
371
+ // As text, as getDescription reads them: the profile selector asks this of every profile too.
372
+ const username = (0, desktops_1.configText)(o.username);
373
+ return o.via ? null : `${username ? `${username}@` : ''}${(0, desktopForm_1.formatAddress)((0, desktops_1.configText)(o.host), (0, osd_1.configNumber)(o.port || 3389))}`;
352
374
  }
353
375
  deleteProfile(profile) {
354
- // A direct desktop's saved account and certificate are its own; one behind a host shares them with that host's
355
- // desktop at the same address.
376
+ // A direct desktop's saved passwords (through any gateway: one it went through before too) and certificate are
377
+ // its own; one behind a host shares them with that host's desktop at the same address.
356
378
  if (!profile.options.via) {
357
- const id = (0, desktops_1.desktopIdOf)(profile.options);
358
- (0, signin_1.forgetCredentials)(`${desktops_1.DIRECT_KEY}#${id}`);
359
- this.injector.get(desktop_service_1.RemoteDesktopService).forgetCertificatesFor(id, true);
360
- this.config.save();
379
+ this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop((0, desktops_1.desktopIdOf)(profile.options), true, `"${profile.name}"`).catch(() => null);
361
380
  }
362
381
  }
363
382
  };
@@ -427,4 +446,3 @@ exports.RDPProfileOpener = RDPProfileOpener = __decorate([
427
446
  (0, core_1.Injectable)(),
428
447
  __metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone])
429
448
  ], RDPProfileOpener);
430
- //# sourceMappingURL=rdpProfile.js.map
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderReleaseSettings = renderReleaseSettings;
4
+ const help_1 = require("./help");
5
+ /** Rebuilt from manager state; every package mutation stays behind the manager's confirmation and validation. */
6
+ function renderReleaseSettings(root, releases, actions) {
7
+ const oldSelection = root.dataset.releaseChannel === releases.state.channel ? root.querySelector('[data-version]')?.value : undefined;
8
+ root.dataset.releaseChannel = releases.state.channel;
9
+ const selected = releases.versions.some(p => p.version === oldSelection) ? oldSelection : releases.recommended ?? '';
10
+ const blocked = releases.busy || releases.checking || !!releases.pending || releases.removed;
11
+ const disable = (condition) => condition ? 'disabled' : '';
12
+ const reason = selected ? releases.reason(selected) : null;
13
+ const stable = releases.catalog?.stable;
14
+ const previous = releases.state.previous;
15
+ const status = releases.checking ? 'Checking…' : releases.checkedAt !== null ? `Last checked ${new Date(releases.checkedAt).toLocaleString()}.` : 'Not checked';
16
+ root.innerHTML = `
17
+ <div class="trd-release-summary">
18
+ <div><span class="trd-sub">Running version</span><h4>tabby-rdp ${(0, help_1.esc)(releases.running)}</h4></div>
19
+ <button class="btn btn-link btn-sm" data-release-notes="${(0, help_1.esc)(releases.running)}">Release notes</button>
20
+ </div>
21
+ ${releases.pending ? `<div class="trd-release-notice" role="status"><b>Restart required to use ${(0, help_1.esc)(releases.pending)}.</b> Close remote desktops and restart Tabby.</div>` : ''}
22
+ ${releases.removed ? '<div class="trd-release-notice" role="status"><b>Plugin uninstalled.</b> Close remote desktops and restart Tabby.</div>' : ''}
23
+ ${releases.notice ? `<p role="status">${(0, help_1.esc)(releases.notice)}</p>` : ''}
24
+ ${releases.storageWarning ? `<p role="alert">${(0, help_1.esc)(releases.storageWarning)}</p>` : ''}
25
+ <div class="form-line">
26
+ <div class="header"><label class="title" for="trd-release-channel">Release channel</label><div class="description">Preview includes beta releases and release candidates.</div></div>
27
+ <select id="trd-release-channel" class="form-control" data-channel ${disable(blocked)}><option value="stable" ${releases.state.channel === 'stable' ? 'selected' : ''}>Stable</option><option value="preview" ${releases.state.channel === 'preview' ? 'selected' : ''}>Preview</option></select>
28
+ </div>
29
+ <label class="trd-release-pause"><input type="checkbox" data-pause ${disable(releases.busy)} ${!releases.state.paused ? 'checked' : ''}> Check for updates automatically</label>
30
+ <div class="trd-release-actions"><button class="btn btn-secondary btn-sm" data-check ${disable(releases.checking || releases.busy)}>Check now</button><span class="trd-sub" role="status">${(0, help_1.esc)(status)}</span></div>
31
+ ${releases.error ? `<p class="trd-release-error" role="alert">${(0, help_1.esc)(releases.error)}${releases.catalog ? ' Showing the last successful check.' : ''}</p>` : ''}
32
+ ${releases.state.channel === 'preview' && releases.catalog && !releases.catalog.preview ? '<p>No preview releases available.</p>' : ''}
33
+ ${releases.recommended ? `<p>Latest: <b>${(0, help_1.esc)(releases.recommended)}</b> <button class="btn btn-link btn-sm" data-release-notes="${(0, help_1.esc)(releases.recommended)}">Release notes</button></p>` : ''}
34
+ <h5><label for="trd-release-version">Version</label></h5>
35
+ <div class="trd-release-actions">
36
+ <select id="trd-release-version" class="form-control" data-version ${disable(blocked || !releases.versions.length)}>
37
+ ${!releases.versions.length ? '<option>No versions loaded</option>' : releases.versions.map(p => {
38
+ const why = releases.reason(p.version);
39
+ return `<option value="${(0, help_1.esc)(p.version)}" ${p.version === selected ? 'selected' : ''} ${disable(!!why)}>${(0, help_1.esc)(p.version)}${why ? ` — ${(0, help_1.esc)(why)}` : ''}</option>`;
40
+ }).join('')}
41
+ </select>
42
+ <button class="btn btn-primary btn-sm" data-install ${disable(blocked || !!releases.error || !selected || !!reason || selected === releases.running)}>Install selected version…</button>
43
+ <button class="btn btn-link btn-sm" data-selected-notes ${disable(!selected)}>Release notes</button>
44
+ </div>
45
+ <p class="trd-sub" data-version-reason ${reason ? '' : 'hidden'}>${(0, help_1.esc)(reason ?? '')}</p>
46
+ <div class="trd-release-actions">
47
+ ${stable && stable !== releases.running ? `<button class="btn btn-secondary btn-sm" data-stable ${disable(blocked || !!releases.error || !!releases.reason(stable))}>Return to stable ${(0, help_1.esc)(stable)}…</button>` : ''}
48
+ <button class="btn btn-secondary btn-sm" data-restore ${disable(blocked || !!releases.error || !previous || !!releases.reason(previous) || previous === releases.running)}>Restore previous version${previous ? ` (${(0, help_1.esc)(previous)})` : ''}…</button>
49
+ </div>
50
+ ${!previous ? '<p class="trd-sub">No previous version saved.</p>' : releases.reason(previous) ? `<p class="trd-sub">Previous version unavailable: ${(0, help_1.esc)(releases.reason(previous))}</p>` : ''}
51
+ <h5>Disable or uninstall</h5>
52
+ <p class="trd-sub">Saved connections, settings, passwords and remote setup are kept.</p>
53
+ <div class="trd-release-actions"><button class="btn btn-secondary btn-sm" data-plugins>Manage in Tabby…</button><button class="btn btn-outline-danger btn-sm" data-uninstall ${disable(blocked)}>Uninstall plugin…</button><a href="#" data-recovery>Cleanup instructions</a></div>`;
54
+ root.querySelector('[data-channel]').addEventListener('change', e => {
55
+ void releases.setChannel(e.target.value);
56
+ });
57
+ root.querySelector('[data-pause]').addEventListener('change', e => { void releases.setPaused(!e.target.checked); });
58
+ root.querySelector('[data-check]').addEventListener('click', () => { void releases.refreshCatalog(true); });
59
+ root.querySelector('[data-version]').addEventListener('change', () => renderReleaseSettings(root, releases, actions));
60
+ root.querySelector('[data-install]').addEventListener('click', () => { void releases.install(selected); });
61
+ root.querySelector('[data-stable]')?.addEventListener('click', () => { if (stable) {
62
+ void releases.install(stable, true);
63
+ } });
64
+ root.querySelector('[data-restore]').addEventListener('click', () => { if (previous) {
65
+ void releases.install(previous);
66
+ } });
67
+ root.querySelector('[data-uninstall]').addEventListener('click', () => { void releases.uninstall(); });
68
+ root.querySelector('[data-plugins]').addEventListener('click', actions.plugins);
69
+ root.querySelector('[data-recovery]').addEventListener('click', e => { e.preventDefault(); actions.link(`${help_1.TROUBLESHOOTING_URL}#recovery`); });
70
+ root.querySelector('[data-selected-notes]').addEventListener('click', () => actions.notes(selected));
71
+ root.querySelectorAll('[data-release-notes]').forEach(el => el.addEventListener('click', () => actions.notes(el.dataset.releaseNotes)));
72
+ }
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReleaseManager = exports.RELEASE_STATE_KEY = exports.RELEASES = exports.REGISTRY = void 0;
4
+ exports.releaseVersion = releaseVersion;
5
+ exports.newer = newer;
6
+ exports.registryJSON = registryJSON;
7
+ exports.parseCatalog = parseCatalog;
8
+ exports.publishedVersions = publishedVersions;
9
+ const semver = require("semver");
10
+ const rxjs_1 = require("rxjs");
11
+ exports.REGISTRY = 'https://registry.npmjs.org/tabby-rdp';
12
+ exports.RELEASES = 'https://github.com/meanaverage/tabby-rdp/releases';
13
+ exports.RELEASE_STATE_KEY = 'tabby-rdp.releases.v1';
14
+ const BASELINE = '0.5.1';
15
+ const CACHE_MS = 15 * 60 * 1000;
16
+ /** Exact, canonical versions only: never an npm selector, alias, URL or tag. */
17
+ function releaseVersion(value) {
18
+ return typeof value === 'string' && semver.valid(value) === value && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:beta|rc)\.(0|[1-9]\d*))?$/.test(value);
19
+ }
20
+ function newer(a, b) {
21
+ return releaseVersion(a) && releaseVersion(b) && semver.gt(a, b);
22
+ }
23
+ const greatest = (a, b) => semver.gt(a, b) ? a : b;
24
+ function policy(value) {
25
+ return value && typeof value.minimumTabbyVersion === 'string' && semver.valid(value.minimumTabbyVersion) &&
26
+ releaseVersion(value.rollbackFloor) ? { minimumTabbyVersion: value.minimumTabbyVersion, rollbackFloor: value.rollbackFloor } : undefined;
27
+ }
28
+ /** Bound both the response body and elapsed time, including slow streaming responses. */
29
+ async function registryJSON(url, timeoutMs = 15000, maxBytes = 4 * 1024 * 1024) {
30
+ const abort = new AbortController();
31
+ const timer = setTimeout(() => abort.abort(), timeoutMs);
32
+ try {
33
+ const response = await fetch(url, { cache: 'no-store', signal: abort.signal });
34
+ if (!response.ok || !response.body) {
35
+ throw new Error(`The registry answered ${response.status}.`);
36
+ }
37
+ const reader = response.body.getReader();
38
+ const chunks = [];
39
+ let size = 0;
40
+ for (;;) {
41
+ const { done, value } = await reader.read();
42
+ if (done) {
43
+ break;
44
+ }
45
+ size += value.byteLength;
46
+ if (size > maxBytes) {
47
+ throw new Error('The registry\'s answer is too large.');
48
+ }
49
+ chunks.push(value);
50
+ }
51
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
52
+ }
53
+ finally {
54
+ clearTimeout(timer);
55
+ abort.abort();
56
+ }
57
+ }
58
+ function parseCatalog(data) {
59
+ if (data?.name !== 'tabby-rdp' || !data.versions || typeof data.versions !== 'object' || Array.isArray(data.versions)) {
60
+ throw new Error('The registry returned an invalid version list.');
61
+ }
62
+ const entries = Object.entries(data.versions);
63
+ if (entries.length > 5000) {
64
+ throw new Error('The registry\'s version list is too large.');
65
+ }
66
+ const versions = [];
67
+ for (const [version, raw] of entries) {
68
+ const p = raw;
69
+ if (!releaseVersion(version) || p?.name !== 'tabby-rdp' || p.version !== version) {
70
+ continue;
71
+ }
72
+ // 0.5.1 predates this field. Its requirements and irreversible credential migration were reviewed explicitly.
73
+ let compatibility = policy(p.tabbyRdp) ?? (version === BASELINE ? { minimumTabbyVersion: '1.0.236', rollbackFloor: BASELINE } : undefined);
74
+ if (compatibility && semver.gt(compatibility.rollbackFloor, version)) {
75
+ compatibility = undefined;
76
+ }
77
+ versions.push({ version, policy: compatibility, node: typeof p.engines?.node === 'string' ? p.engines.node : undefined,
78
+ os: Array.isArray(p.os) ? p.os.filter((x) => typeof x === 'string') : undefined,
79
+ cpu: Array.isArray(p.cpu) ? p.cpu.filter((x) => typeof x === 'string') : undefined,
80
+ deprecated: !!p.deprecated });
81
+ }
82
+ versions.sort((a, b) => semver.rcompare(a.version, b.version));
83
+ const tag = (name, preview) => {
84
+ const v = data['dist-tags']?.[name];
85
+ return releaseVersion(v) && !!semver.prerelease(v) === preview && versions.some(p => p.version === v) ? v : null;
86
+ };
87
+ return { versions, stable: tag('latest', false), preview: tag('beta', true) };
88
+ }
89
+ async function publishedVersions() {
90
+ return parseCatalog(await registryJSON(exports.REGISTRY));
91
+ }
92
+ /** No config or credential backups: only local preferences and successful package-change history. */
93
+ class ReleaseManager {
94
+ constructor(env, initiallyPaused = false) {
95
+ this.env = env;
96
+ this.changed$ = new rxjs_1.Subject();
97
+ this.catalog = null;
98
+ this.checking = false;
99
+ this.busy = false;
100
+ this.error = '';
101
+ this.catalogError = '';
102
+ this.notice = '';
103
+ this.storageWarning = '';
104
+ this.checkedAt = null;
105
+ this.lastAttempt = -Infinity;
106
+ this.running = env.running;
107
+ this.now = env.now ?? Date.now;
108
+ this.state = { channel: 'stable', paused: initiallyPaused, rollbackFloor: greatest(BASELINE, env.policy.rollbackFloor) };
109
+ this.syncFromStorage();
110
+ this.ready = this.lock(async () => {
111
+ this.syncFromStorage();
112
+ this.reconcileDisk();
113
+ this.persist();
114
+ }).catch(e => { this.error = this.message(e); }).finally(() => this.changed$.next());
115
+ }
116
+ message(error) { return error instanceof Error ? error.message : 'Use Tabby Plugins to manage this installation.'; }
117
+ async lock(run) {
118
+ if (this.env.withLock) {
119
+ await this.env.withLock(run);
120
+ }
121
+ else {
122
+ await run();
123
+ }
124
+ }
125
+ /** Storage events call this too. Reading never clears another window's successful package change. */
126
+ syncFromStorage() {
127
+ if (!this.env.storage) {
128
+ return;
129
+ }
130
+ try {
131
+ const saved = JSON.parse(this.env.storage.getItem(exports.RELEASE_STATE_KEY) ?? 'null');
132
+ if (!saved) {
133
+ return;
134
+ }
135
+ this.state.channel = saved.channel === 'preview' ? 'preview' : 'stable';
136
+ if (typeof saved.paused === 'boolean') {
137
+ this.state.paused = saved.paused;
138
+ }
139
+ this.state.previous = releaseVersion(saved.previous) ? saved.previous : undefined;
140
+ this.state.pending = releaseVersion(saved.pending) ? saved.pending : undefined;
141
+ this.state.removed = saved.removed === true;
142
+ if (releaseVersion(saved.rollbackFloor)) {
143
+ this.state.rollbackFloor = greatest(this.state.rollbackFloor, saved.rollbackFloor);
144
+ }
145
+ }
146
+ catch { /* Keep the known state if local storage is temporarily unavailable. */ }
147
+ this.changed$.next();
148
+ }
149
+ /** Under the shared lock, so a second window never observes our installer's intermediate files. */
150
+ reconcileDisk() {
151
+ if (!this.env.diskVersion) {
152
+ return;
153
+ }
154
+ const disk = this.env.diskVersion();
155
+ if (disk !== null && !releaseVersion(disk)) {
156
+ throw new Error('The installed package version cannot be verified. Use Tabby Plugins.');
157
+ }
158
+ if (disk === null) {
159
+ this.state.removed = true;
160
+ }
161
+ else {
162
+ if ((this.state.pending && this.state.pending !== disk) || this.state.removed) {
163
+ this.notice = `The package changed outside this page. Tabby is running ${this.running}; the package on disk is ${disk}.`;
164
+ this.state.previous = undefined;
165
+ }
166
+ this.state.pending = disk;
167
+ this.state.removed = false;
168
+ }
169
+ }
170
+ get pending() { return !this.removed && this.state.pending && this.state.pending !== this.running ? this.state.pending : null; }
171
+ get removed() { return this.state.removed === true; }
172
+ persist() {
173
+ try {
174
+ if (!this.env.storage) {
175
+ throw new Error('Local storage unavailable');
176
+ }
177
+ this.env.storage.setItem(exports.RELEASE_STATE_KEY, JSON.stringify(this.state));
178
+ this.storageWarning = '';
179
+ }
180
+ catch {
181
+ this.storageWarning = 'Version preferences could not be saved on this computer. They apply to this window only. Manage installations in Tabby Plugins.';
182
+ }
183
+ }
184
+ async preference(change) {
185
+ if (this.busy) {
186
+ return;
187
+ }
188
+ await this.ready;
189
+ try {
190
+ await this.lock(async () => {
191
+ this.syncFromStorage();
192
+ Object.assign(this.state, change);
193
+ this.persist();
194
+ });
195
+ }
196
+ catch (e) {
197
+ this.error = this.message(e);
198
+ }
199
+ this.changed$.next();
200
+ }
201
+ async setChannel(value) {
202
+ if (value === 'stable' || value === 'preview') {
203
+ await this.preference({ channel: value });
204
+ }
205
+ }
206
+ async setPaused(paused) { await this.preference({ paused }); }
207
+ get recommended() {
208
+ const stable = this.catalog?.stable ?? null;
209
+ const preview = this.state.channel === 'preview' ? this.catalog?.preview : null;
210
+ return preview && (!stable || newer(preview, stable)) ? preview : stable;
211
+ }
212
+ get available() {
213
+ const version = this.recommended;
214
+ return !this.state.paused && !this.catalogError && !this.pending && !this.removed && version && newer(version, this.running) && !this.reason(version) ? version : null;
215
+ }
216
+ get versions() {
217
+ return (this.catalog?.versions ?? []).filter(p => this.state.channel === 'preview' || !semver.prerelease(p.version));
218
+ }
219
+ /** A reason to refuse a version, independent of whether it is newer. Return-to-stable may downgrade. */
220
+ reason(version) {
221
+ if (!releaseVersion(version)) {
222
+ return 'Invalid version.';
223
+ }
224
+ const p = this.catalog?.versions.find(x => x.version === version);
225
+ if (!p) {
226
+ return 'This version is not in the published version list.';
227
+ }
228
+ if (p.deprecated) {
229
+ return 'This version has been withdrawn from recommendations.';
230
+ }
231
+ if (semver.lt(version, this.state.rollbackFloor)) {
232
+ return `Saved data requires version ${this.state.rollbackFloor} or newer.`;
233
+ }
234
+ if (!p.policy) {
235
+ return 'Compatibility has not been confirmed for this version.';
236
+ }
237
+ if (!semver.valid(this.env.tabby) || semver.lt(this.env.tabby, p.policy.minimumTabbyVersion)) {
238
+ return `Requires Tabby ${p.policy.minimumTabbyVersion} or newer.`;
239
+ }
240
+ if (p.node && (!semver.validRange(p.node) || !semver.satisfies(this.env.node, p.node, { includePrerelease: true }))) {
241
+ return 'Requires a different Node.js version. Update Tabby first.';
242
+ }
243
+ const supports = (list, value) => !list?.length ||
244
+ (!list.includes(`!${value}`) && (list.includes('any') || !list.some(x => !x.startsWith('!')) || list.includes(value)));
245
+ if (!supports(p.os, this.env.os) || !supports(p.cpu, this.env.cpu)) {
246
+ return 'Not available for this computer.';
247
+ }
248
+ return null;
249
+ }
250
+ /** One shared request; ordinary reads cache for 15 minutes, explicit checks retry at most every 10 seconds. */
251
+ async refreshCatalog(force = false) {
252
+ if (this.inFlight) {
253
+ return this.inFlight;
254
+ }
255
+ const elapsed = this.now() - this.lastAttempt;
256
+ if (elapsed < (force ? 10000 : CACHE_MS)) {
257
+ return;
258
+ }
259
+ this.lastAttempt = this.now();
260
+ this.checking = true;
261
+ this.error = '';
262
+ this.changed$.next();
263
+ this.inFlight = (async () => {
264
+ try {
265
+ this.catalog = await Promise.resolve().then(() => this.env.fetchCatalog());
266
+ this.catalogError = '';
267
+ this.checkedAt = this.now();
268
+ }
269
+ catch (e) {
270
+ this.error = this.catalogError = `Could not check published versions. ${e instanceof Error ? e.message : 'Try again later.'}`;
271
+ }
272
+ finally {
273
+ this.checking = false;
274
+ this.inFlight = undefined;
275
+ this.changed$.next();
276
+ }
277
+ })();
278
+ return this.inFlight;
279
+ }
280
+ async install(version, returnToStable = false) {
281
+ if (this.busy) {
282
+ return;
283
+ }
284
+ this.busy = true;
285
+ this.error = '';
286
+ this.changed$.next();
287
+ const progress = (phase, message) => this.env.progress?.({ action: 'install', phase, version, message });
288
+ try {
289
+ progress('preparing');
290
+ await this.ready;
291
+ await this.lock(async () => {
292
+ this.syncFromStorage();
293
+ this.reconcileDisk();
294
+ this.persist();
295
+ if (this.storageWarning) {
296
+ throw new Error('Version history cannot be saved. Use Tabby Plugins to install.');
297
+ }
298
+ if (this.pending) {
299
+ progress('success', `tabby-rdp ${this.pending} is already installed. Restart Tabby to use it.`);
300
+ return;
301
+ }
302
+ if (this.removed) {
303
+ throw new Error('The plugin has been uninstalled. Restart Tabby before changing versions.');
304
+ }
305
+ await this.refreshCatalog();
306
+ if (this.catalogError) {
307
+ throw new Error('Check published versions successfully before installing.');
308
+ }
309
+ const reason = this.reason(version);
310
+ if (reason) {
311
+ throw new Error(reason);
312
+ }
313
+ if (version === this.running) {
314
+ progress('success', `tabby-rdp ${version} is already running.`);
315
+ return;
316
+ }
317
+ if (semver.prerelease(version) && this.state.channel !== 'preview') {
318
+ throw new Error('Choose Preview before installing a preview version.');
319
+ }
320
+ if (returnToStable && version !== this.catalog?.stable) {
321
+ throw new Error('Check the current stable version again.');
322
+ }
323
+ const downgrade = semver.lt(version, this.running);
324
+ if (!await this.env.confirm(`Install tabby-rdp ${version}?`, `Currently running ${this.running}. ${downgrade ? 'This is a downgrade. ' : ''}Close remote desktops and restart Tabby after installation.`, 'Install')) {
325
+ progress('cancelled');
326
+ return;
327
+ }
328
+ progress('installing');
329
+ await this.env.install('tabby-rdp', version);
330
+ progress('verifying');
331
+ if (this.env.diskVersion) {
332
+ const installed = this.env.diskVersion();
333
+ if (installed !== version) {
334
+ throw new Error(installed === null ? 'The installed plugin package is missing.' :
335
+ `The installed version is ${installed}; expected ${version}.`);
336
+ }
337
+ }
338
+ // History is committed only after the supported installer succeeds.
339
+ this.state.previous = this.running;
340
+ this.state.pending = version;
341
+ this.state.removed = false;
342
+ if (returnToStable) {
343
+ this.state.channel = 'stable';
344
+ }
345
+ this.persist();
346
+ progress('success');
347
+ });
348
+ }
349
+ catch (e) {
350
+ this.error = `Installation did not complete. ${this.message(e)}`;
351
+ progress('error', this.error);
352
+ }
353
+ finally {
354
+ this.busy = false;
355
+ this.changed$.next();
356
+ }
357
+ }
358
+ async uninstall() {
359
+ if (this.busy) {
360
+ return;
361
+ }
362
+ this.busy = true;
363
+ this.error = '';
364
+ this.changed$.next();
365
+ const progress = (phase, message) => this.env.progress?.({ action: 'uninstall', phase, message });
366
+ try {
367
+ progress('preparing');
368
+ await this.ready;
369
+ await this.lock(async () => {
370
+ this.syncFromStorage();
371
+ this.reconcileDisk();
372
+ this.persist();
373
+ if (this.storageWarning) {
374
+ throw new Error('Version history cannot be saved. Use Tabby Plugins to uninstall.');
375
+ }
376
+ if (this.pending) {
377
+ throw new Error('A version change is pending. Restart Tabby before uninstalling.');
378
+ }
379
+ if (this.removed) {
380
+ progress('success', 'tabby-rdp is already uninstalled. Restart Tabby to finish.');
381
+ return;
382
+ }
383
+ if (!await this.env.confirm('Uninstall tabby-rdp?', 'Close remote desktops before uninstalling, then restart Tabby. Saved connections, settings, passwords and remote setup are kept.', 'Uninstall')) {
384
+ progress('cancelled');
385
+ return;
386
+ }
387
+ progress('uninstalling');
388
+ await this.env.uninstall('tabby-rdp');
389
+ progress('verifying');
390
+ if (this.env.diskVersion && this.env.diskVersion() !== null) {
391
+ throw new Error('The plugin package is still installed.');
392
+ }
393
+ this.state.removed = true;
394
+ this.persist();
395
+ progress('success');
396
+ });
397
+ }
398
+ catch (e) {
399
+ this.error = `Uninstall did not complete. ${this.message(e)}`;
400
+ progress('error', this.error);
401
+ }
402
+ finally {
403
+ this.busy = false;
404
+ this.changed$.next();
405
+ }
406
+ }
407
+ }
408
+ exports.ReleaseManager = ReleaseManager;