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
package/dist/signin.js CHANGED
@@ -3,13 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.STYLE = exports.VAULT_SECRET_TYPE = void 0;
4
4
  exports.useVault = useVault;
5
5
  exports.storeName = storeName;
6
+ exports.forgetOnce = forgetOnce;
7
+ exports.renameCredentials = renameCredentials;
6
8
  exports.loadCredentials = loadCredentials;
7
9
  exports.hasCredentials = hasCredentials;
8
10
  exports.saveCredentials = saveCredentials;
11
+ exports.saveCredentialsIf = saveCredentialsIf;
12
+ exports.forgetMark = forgetMark;
13
+ exports.forgottenSince = forgottenSince;
9
14
  exports.forgetCredentials = forgetCredentials;
10
15
  exports.forgetCredentialsOrFail = forgetCredentialsOrFail;
16
+ exports.forgetCredentialsIf = forgetCredentialsIf;
17
+ exports.gatewayScope = gatewayScope;
18
+ exports.isFor = isFor;
11
19
  exports.forgetCredentialsFor = forgetCredentialsFor;
12
20
  exports.moveCredentialsFor = moveCredentialsFor;
21
+ exports.forgetFormerKey = forgetFormerKey;
13
22
  exports.askCredentials = askCredentials;
14
23
  /**
15
24
  * Sign-in for desktops that use their own accounts (Windows): a form in the desktop layer, and where passwords are
@@ -17,6 +26,7 @@ exports.askCredentials = askCredentials;
17
26
  * keychain.
18
27
  */
19
28
  const desktops_1 = require("./desktops");
29
+ const gateway_1 = require("./gateway");
20
30
  const KEYCHAIN_SERVICE = 'tabby-rdp';
21
31
  /** The Vault secret type of a desktop's or account's password; its key is `{ key, name }` (see vaultKey). */
22
32
  exports.VAULT_SECRET_TYPE = 'rdp:password';
@@ -42,8 +52,14 @@ function storeName() {
42
52
  * back, so two at once (two desktops signing in together) would lose one's change.
43
53
  */
44
54
  let vaultQueue = Promise.resolve();
55
+ /** Same-origin windows share these locks. Store work holds the lock through its actual completion. */
56
+ async function storeLock(name, work, signal) {
57
+ const locks = globalThis.navigator?.locks;
58
+ return locks ? await locks.request(`tabby-rdp.credentials.${name}`, signal ? { signal } : {}, work) : work();
59
+ }
45
60
  function queued(work) {
46
- const next = vaultQueue.then(work, work);
61
+ const run = () => storeLock('vault', work);
62
+ const next = vaultQueue.then(run, run);
47
63
  vaultQueue = next.catch(() => null);
48
64
  return next;
49
65
  }
@@ -52,8 +68,9 @@ function vaultKey(key) {
52
68
  return { key };
53
69
  }
54
70
  function vaultName(key) {
55
- const [who, address] = key.split('#', 2);
56
- return who === 'account' ? `saved account` : who === desktops_1.DIRECT_KEY ? `${address} (direct)` : address ? `${address} behind ${who}` : who;
71
+ const { target, desktopId, gateway } = (0, desktops_1.keyParts)(key);
72
+ const where = `${desktopId}${gateway ? gateway.replace('@gateway#', ' through gateway ') : ''}`;
73
+ return target === 'account' ? `saved account` : target === desktops_1.DIRECT_KEY ? `${where} (direct)` : desktopId ? `${where} behind ${target}` : target;
57
74
  }
58
75
  // The system keychain can keep a call waiting indefinitely: on Linux, a locked or missing keyring waits for an unlock
59
76
  // prompt, which a session without a keyring prompter never shows. Give up after a while. A call that never returns
@@ -81,16 +98,21 @@ function keytar() {
81
98
  */
82
99
  function keychain(call) {
83
100
  const run = () => new Promise((resolve, reject) => {
84
- const store = keytar();
85
- if (!store) {
86
- resolve(undefined);
101
+ if (keychainStuck) {
102
+ reject(new Error('the keychain did not answer earlier'));
87
103
  return;
88
104
  }
105
+ const abort = new AbortController();
89
106
  const timer = setTimeout(() => {
90
107
  keychainStuck = true;
108
+ abort.abort(); // cancels a lock still waiting, never a native call already running
91
109
  reject(new Error('the keychain did not answer'));
92
110
  }, KEYCHAIN_TIMEOUT_MS);
93
- Promise.resolve(call(store)).then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
111
+ // Timeout is outside the lock: a late native write keeps other windows' writes behind it until it ends.
112
+ storeLock('keychain', async () => {
113
+ const store = keytar();
114
+ return store ? call(store) : undefined;
115
+ }, abort.signal).then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
94
116
  });
95
117
  const next = keychainQueue.then(run, run);
96
118
  keychainQueue = next.catch(() => null);
@@ -105,12 +127,198 @@ function parseCredentials(saved) {
105
127
  return null;
106
128
  }
107
129
  }
130
+ let once = null;
131
+ let onceRunning = null;
132
+ /**
133
+ * Forgets the saved passwords whose key `forgets` matches, once in this computer's keychain and once in the Vault,
134
+ * before the store is next used: when it is asked anyway, so that a locked Vault asks for its passphrase at a sign-in
135
+ * rather than when Tabby starts. Each part's mark is read as that part is about to be done (another Tabby window may
136
+ * have done it since this one asked). A part that can't be done then (the keychain doesn't answer, or can't list what
137
+ * it holds; the Vault's prompt is cancelled) is tried again at the next use, and until it is done, what it would
138
+ * forget isn't looked up in that store (see withheld): the desktop asks.
139
+ */
140
+ function forgetOnce(mark, forgets, vault) {
141
+ once = { mark, forgets, vault, keychainDone: false };
142
+ }
143
+ /** Does what forgetOnce and forgetLater left to do, if anything, and waits for it to end. */
144
+ async function used() {
145
+ if (once && !onceRunning) {
146
+ const job = once;
147
+ onceRunning = doOnce(job).catch(() => undefined).finally(() => { onceRunning = null; });
148
+ }
149
+ await onceRunning;
150
+ // A previous background pass may have failed before the store became available. This use gets one fresh attempt.
151
+ await laterRunning;
152
+ if (later.length && !laterRunning) {
153
+ laterRunning = doLater().catch(() => undefined).finally(() => { laterRunning = null; });
154
+ }
155
+ await laterRunning;
156
+ }
157
+ /** forgetOnce's work: the keychain's part first, and on its own, so a Vault that can't be read doesn't hold it up. */
158
+ async function doOnce(job) {
159
+ let forgets;
160
+ try {
161
+ forgets = job.forgets();
162
+ }
163
+ catch {
164
+ return;
165
+ }
166
+ if (!job.keychainDone) {
167
+ try {
168
+ job.keychainDone = await keychainOnce(job.mark, forgets);
169
+ }
170
+ catch { }
171
+ }
172
+ if (!job.vault.done()) {
173
+ try {
174
+ if (vaultOn()) {
175
+ // Read in full or not at all: a passphrase prompt cancelled throws, and the part is tried again.
176
+ const contents = await vault.load();
177
+ for (const secret of contents?.secrets ?? []) {
178
+ if (secret.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string' && forgets(secret.key.key)) {
179
+ await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(secret.key.key)));
180
+ }
181
+ }
182
+ }
183
+ // With the Vault off there is nothing in it: Tabby drops its contents when it is turned off.
184
+ job.vault.markDone();
185
+ }
186
+ catch { }
187
+ }
188
+ if (job.keychainDone && job.vault.done() && once === job) {
189
+ once = null;
190
+ }
191
+ }
192
+ /**
193
+ * This computer's keychain's part of forgetOnce: done before (its mark is there), or now, and marked. True without a
194
+ * keychain, where there is nothing to do. Throws when the keychain doesn't answer, or can't list what it holds: a
195
+ * part done on a listing that came back empty because of that would leave what it should have forgotten in place.
196
+ */
197
+ async function keychainOnce(mark, forgets) {
198
+ const marked = await keychain(store => store.getPassword(KEYCHAIN_SERVICE, mark));
199
+ if (marked === undefined || marked) {
200
+ return true;
201
+ }
202
+ const entries = await keychain(store => store.findCredentials(KEYCHAIN_SERVICE));
203
+ for (const { account } of entries ?? []) {
204
+ if (typeof account === 'string' && account !== mark && forgets(account)) {
205
+ await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, account));
206
+ }
207
+ }
208
+ await keychain(store => store.setPassword(KEYCHAIN_SERVICE, mark, 'done'));
209
+ return true;
210
+ }
211
+ /**
212
+ * Forgets that couldn't be done when they were asked for (see forgetLater), each of one store's part: the Vault's
213
+ * (`inVault`), or the keychain's. Each is done the next time its store is used and can be read (see used, doLater),
214
+ * and what it would forget isn't looked up in that store meanwhile (see withheld). `saved`: keys a password was saved
215
+ * under in that store since, which is a new one, and stays. Kept in this window's memory, for as long as it is open.
216
+ */
217
+ const later = [];
218
+ let laterRunning = null;
219
+ /**
220
+ * The saved passwords whose key `forgets` matches are to go from one store (the Vault, or with `inVault` false the
221
+ * keychain), and couldn't now (it couldn't be listed or wouldn't forget: a Vault whose passphrase prompt was
222
+ * cancelled, a keychain that didn't answer): forgotten once that store can be read (see doLater), and not used from it
223
+ * meanwhile, so that the desktop asks. The other store's part doesn't wait for it.
224
+ */
225
+ function forgetLater(forgets, inVault) {
226
+ later.push({ forgets, inVault, saved: new Set() });
227
+ }
228
+ /**
229
+ * forgetLater's work, each store's part on its own, so that one that can't be read doesn't hold up the other: the
230
+ * keychain's whenever it can be listed; the Vault's at once while the Vault is off (there is nothing in it then: Tabby
231
+ * drops its contents when it is turned off), and otherwise once it is open. Not at the cost of a passphrase prompt of
232
+ * its own: with the Vault locked, its part waits for the Vault's next unlock (by whatever unlocks it, a password looked
233
+ * up or saved), and what it covers isn't used meanwhile.
234
+ */
235
+ async function doLater() {
236
+ for (const inVault of [true, false]) {
237
+ const jobs = later.filter(job => job.inVault === inVault);
238
+ if (!jobs.length || inVault && vaultOn() && !vaultOpen()) {
239
+ continue;
240
+ }
241
+ const entries = await entriesOf(inVault);
242
+ if (!entries) {
243
+ continue;
244
+ }
245
+ for (const job of jobs) {
246
+ if (await forgetListed(inVault, entries, job.forgets, job.saved)) {
247
+ later.splice(later.indexOf(job), 1);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ function vaultOpen() {
253
+ try {
254
+ return !!vault?.isOpen();
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ }
260
+ /**
261
+ * Whether `key` is one forgetOnce's work would forget from that store (the Vault, or with `inVault` false the
262
+ * keychain), and that part isn't done yet (see used), or one a forget left for later would forget from that store (see
263
+ * forgetLater): what is saved under it there isn't used meanwhile.
264
+ */
265
+ function withheld(key, inVault) {
266
+ if (later.some(job => job.inVault === inVault && !job.saved.has(key) && job.forgets(key))) {
267
+ return true;
268
+ }
269
+ const job = once;
270
+ if (!job || (inVault ? job.vault.done() : job.keychainDone)) {
271
+ return false;
272
+ }
273
+ try {
274
+ return job.forgets()(key);
275
+ }
276
+ catch {
277
+ return true;
278
+ }
279
+ }
280
+ /**
281
+ * Targets whose key changed (see RemoteTargets.keyChanged): the key each had up to 0.5.0, by the key it has now. A
282
+ * password saved under the former one moves to the new one the first time it is looked for there (see loadCredentials):
283
+ * when its store is asked anyway, so a locked Vault asks for its passphrase at a sign-in rather than when a tab opens.
284
+ */
285
+ const formerKeys = new Map();
286
+ /** A target's key changed from `from` to `to` (see formerKeys). */
287
+ function renameCredentials(from, to) {
288
+ if (from !== to) {
289
+ formerKeys.set(to, from);
290
+ }
291
+ }
108
292
  /**
109
293
  * Saved credentials for a desktop (keyed by its session key) or an account (`account#<id>`), or null. With the Vault
110
- * on, the keychain is still read for what was saved before it was turned on.
294
+ * on, the keychain is still read for what was saved before it was turned on. Credentials saved under the target's
295
+ * former key (see formerKeys) are moved under this one, and returned.
111
296
  */
112
297
  async function loadCredentials(key) {
113
- if (vaultOn()) {
298
+ await used();
299
+ const found = await loadSaved(key);
300
+ if (found) {
301
+ return found;
302
+ }
303
+ for (const [to, from] of formerKeys) {
304
+ const former = (0, desktops_1.rekeyed)(key, to, from);
305
+ if (former) {
306
+ const moved = await loadSaved(former);
307
+ if (moved) {
308
+ // Forgotten there only once saved here: should the store refuse, it is found there again next time.
309
+ try {
310
+ await saveCredentials(key, moved);
311
+ await forgetCredentialsOrFail(former);
312
+ }
313
+ catch { }
314
+ }
315
+ return moved;
316
+ }
317
+ }
318
+ return null;
319
+ }
320
+ async function loadSaved(key) {
321
+ if (vaultOn() && !withheld(key, true)) {
114
322
  try {
115
323
  const secret = await vault.getSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
116
324
  const found = parseCredentials(secret?.value);
@@ -120,6 +328,9 @@ async function loadCredentials(key) {
120
328
  }
121
329
  catch { }
122
330
  }
331
+ if (withheld(key, false)) {
332
+ return null;
333
+ }
123
334
  try {
124
335
  return parseCredentials(await keychain(store => store.getPassword(KEYCHAIN_SERVICE, key)));
125
336
  }
@@ -158,16 +369,97 @@ async function hasCredentials(key) {
158
369
  }
159
370
  }
160
371
  async function saveCredentials(key, credentials, label) {
372
+ await saveCredentialsIf(key, credentials, () => true, label);
373
+ }
374
+ /**
375
+ * Checks a revision within the store queue/lock, including after an asynchronous write. If invalidated while writing,
376
+ * removes that write before a newer queued replacement can run. Never schedules a deletion behind a newer write.
377
+ */
378
+ async function saveCredentialsIf(key, credentials, current, label) {
379
+ await used();
161
380
  if (vaultOn()) {
162
- await queued(() => vault.addSecret({ type: exports.VAULT_SECRET_TYPE, key: { ...vaultKey(key), description: label ?? vaultName(key) }, value: JSON.stringify(credentials) }));
381
+ const saved = await queued(async () => {
382
+ if (!current()) {
383
+ return false;
384
+ }
385
+ await vault.addSecret({ type: exports.VAULT_SECRET_TYPE, key: { ...vaultKey(key), description: label ?? vaultName(key) }, value: JSON.stringify(credentials) });
386
+ if (!current()) {
387
+ await vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
388
+ return false;
389
+ }
390
+ return true;
391
+ });
392
+ if (!saved) {
393
+ return false;
394
+ }
395
+ newlySaved(key, true);
163
396
  // Not in two places: what the keychain had for it goes.
164
397
  try {
165
- await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, key));
398
+ await keychain(async (store) => { if (current()) {
399
+ await store.deletePassword(KEYCHAIN_SERVICE, key);
400
+ } });
166
401
  }
167
402
  catch { }
168
- return;
403
+ return true;
404
+ }
405
+ const saved = await keychain(async (store) => {
406
+ if (!current()) {
407
+ return false;
408
+ }
409
+ await store.setPassword(KEYCHAIN_SERVICE, key, JSON.stringify(credentials));
410
+ if (!current()) {
411
+ await store.deletePassword(KEYCHAIN_SERVICE, key);
412
+ return false;
413
+ }
414
+ return true;
415
+ });
416
+ if (saved) {
417
+ newlySaved(key, false);
418
+ }
419
+ return !!saved;
420
+ }
421
+ /**
422
+ * A password was saved under `key` just now, in the Vault or with `inVault` false the keychain, in place of what a
423
+ * forget left for later would forget from that store (see forgetLater): it stays. (What the other store has under that
424
+ * key is still the old one.) The rest of such a forget is done now, while the store that took the password can be read.
425
+ */
426
+ function newlySaved(key, inVault) {
427
+ for (const job of later) {
428
+ if (job.inVault === inVault) {
429
+ job.saved.add(key);
430
+ }
431
+ }
432
+ if (later.length) {
433
+ used().catch(() => undefined);
169
434
  }
170
- await keychain(store => store.setPassword(KEYCHAIN_SERVICE, key, JSON.stringify(credentials)));
435
+ }
436
+ /**
437
+ * The latest forgets (a password forgotten for a key, an edit or a removal of a desktop, the passwords under a former
438
+ * key), each with a number and what it covers. A sign-in that began before one and ends after it, the server having
439
+ * taken its time, mustn't save its password again, which would undo the forget (see forgottenSince). Only the last
440
+ * FORGETS_KEPT are kept.
441
+ */
442
+ const FORGETS_KEPT = 256;
443
+ const forgotten = [];
444
+ let forgetCount = 0;
445
+ /** Records a forget, as it is asked for: what began before it doesn't save under the keys it covers. */
446
+ function noteForget(covers) {
447
+ forgotten.push({ n: ++forgetCount, covers });
448
+ if (forgotten.length > FORGETS_KEPT) {
449
+ forgotten.shift();
450
+ }
451
+ }
452
+ /** Where the forgets stand now: what a sign-in takes when its credentials are, to ask forgottenSince when it ends. */
453
+ function forgetMark() {
454
+ return forgetCount;
455
+ }
456
+ /**
457
+ * Whether a forget covering `key` was asked for since `mark` (see forgetMark): the desktop was edited or removed, or
458
+ * what was saved for it forgotten, while a sign-in ended later was under way. A mark older than the forgets kept counts
459
+ * as forgotten since: a sign-in that slow asks again, at worst.
460
+ */
461
+ function forgottenSince(key, mark) {
462
+ return forgetCount - mark > forgotten.length || forgotten.some(f => f.n > mark && f.covers(key));
171
463
  }
172
464
  async function forgetCredentials(key) {
173
465
  try {
@@ -177,56 +469,195 @@ async function forgetCredentials(key) {
177
469
  }
178
470
  /** As forgetCredentials, but a store that wouldn't (a Vault prompt cancelled, a keychain not answering) is reported. */
179
471
  async function forgetCredentialsOrFail(key) {
472
+ noteForget(k => k === key);
473
+ await used();
474
+ return forgetNow(key);
475
+ }
476
+ /** An account edit/removal must not delete a replacement committed by a later edit. */
477
+ async function forgetCredentialsIf(key, current) {
478
+ noteForget(k => k === key);
479
+ await used();
180
480
  if (vaultOn()) {
181
- await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key)));
481
+ await queued(async () => { if (current()) {
482
+ await vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
483
+ } });
484
+ }
485
+ await keychain(async (store) => { if (current()) {
486
+ await store.deletePassword(KEYCHAIN_SERVICE, key);
487
+ } });
488
+ }
489
+ /** Forgets what is saved under `key`, in both stores. */
490
+ async function forgetNow(key) {
491
+ await forgetIn(true, key);
492
+ await forgetIn(false, key);
493
+ }
494
+ /** Forgets what is saved under `key` in one store: the Vault (while it is on), or with `inVault` false the keychain. */
495
+ async function forgetIn(inVault, key) {
496
+ if (inVault) {
497
+ if (vaultOn()) {
498
+ await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key)));
499
+ }
500
+ return;
182
501
  }
183
502
  // No keychain at all is not a failure to forget; one that didn't answer is.
184
503
  await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, key));
185
504
  }
186
- /** Every saved entry's key and value, from the Vault (when on) and the keychain. */
187
- async function allCredentials() {
188
- const entries = [];
189
- if (vaultOn()) {
190
- try {
191
- const contents = await vault.load();
192
- for (const secret of contents?.secrets ?? []) {
193
- if (secret.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string') {
194
- entries.push({ account: secret.key.key, password: secret.value });
195
- }
196
- }
505
+ /**
506
+ * Every entry saved in one store, by its key: the Vault's (none while it is off: Tabby drops its contents when it is
507
+ * turned off), or with `inVault` false the keychain's (none without one). Null where that store can't be listed in full
508
+ * now (a Vault whose passphrase prompt was cancelled, a keychain that doesn't answer): what goes by such a list,
509
+ * forgetting or moving a desktop's passwords, would leave the rest in place, and in use. Each store is listed on its
510
+ * own, so that one that can't be doesn't keep what the other holds from being done.
511
+ */
512
+ async function entriesOf(inVault) {
513
+ try {
514
+ if (!inVault) {
515
+ const entries = await keychain(store => store.findCredentials(KEYCHAIN_SERVICE));
516
+ return (entries ?? []).filter(entry => typeof entry?.account === 'string');
197
517
  }
198
- catch { }
518
+ if (!vaultOn()) {
519
+ return [];
520
+ }
521
+ const contents = await vault.load();
522
+ return (contents?.secrets ?? []).filter((secret) => secret?.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string')
523
+ .map((secret) => ({ account: secret.key.key, password: secret.value }));
199
524
  }
200
- try {
201
- entries.push(...await keychain(store => store.findCredentials(KEYCHAIN_SERVICE)) ?? []);
525
+ catch {
526
+ return null;
202
527
  }
203
- catch { }
204
- return entries;
205
528
  }
206
- /** Whether a saved account is for a desktop at this address behind some SSH host (not a direct one: `rdp#<id>`). */
207
- function isBehindHost(account, desktopId) {
208
- return account.endsWith(`#${desktopId}`) && !account.startsWith(`${desktops_1.DIRECT_KEY}#`);
529
+ /**
530
+ * Forgets from one store (the Vault, or with `inVault` false the keychain) what `entries`, all it holds (see
531
+ * entriesOf), has under a key `forgets` matches, but for those `kept`. False where it wouldn't forget one of them.
532
+ */
533
+ async function forgetListed(inVault, entries, forgets, kept) {
534
+ let done = true;
535
+ for (const key of new Set(entries.map(({ account }) => account))) {
536
+ if (!kept?.has(key) && forgets(key)) {
537
+ try {
538
+ await forgetIn(inVault, key);
539
+ }
540
+ catch {
541
+ done = false;
542
+ }
543
+ }
544
+ }
545
+ return done;
546
+ }
547
+ /**
548
+ * What a saved desktop password's key adds for the RD Gateway it goes through (`@gateway#host:port`, the gateway as
549
+ * parseGateway has it, so another spelling of it is the same one), or '' for none. Whenever a desktop has a gateway,
550
+ * its password is kept for that gateway too, a gateway account of its own or not: a password saved for one gateway
551
+ * (or for a connection without one) is never sent through a different gateway named for the same address, which could
552
+ * lead somewhere else, without asking.
553
+ */
554
+ function gatewayScope(gateway) {
555
+ const parsed = gateway ? (0, gateway_1.parseGateway)(gateway) : null;
556
+ return parsed ? `@gateway#${parsed.host}:${parsed.port}` : '';
557
+ }
558
+ /**
559
+ * Whether a credential key is one of a desktop's saved passwords (see SavedFor). Anchored on the key's parts, so an
560
+ * unrelated key that merely ends in `#<id>` (a gateway pin, or a desktop whose own id is a suffix of another's) isn't
561
+ * matched; a gateway suffix on the key doesn't change the id.
562
+ */
563
+ function isFor(account, desktopId, which) {
564
+ const { target, desktopId: id, gateway } = (0, desktops_1.keyParts)(account);
565
+ return id === desktopId && (which.direct ? target === desktops_1.DIRECT_KEY : target !== desktops_1.DIRECT_KEY && target !== 'gateway' && target !== 'account') &&
566
+ (which.scope === undefined || gateway === which.scope);
209
567
  }
210
- /** Forgets saved accounts for a desktop on any SSH host (session keys ending in `#<id>`). */
211
- async function forgetCredentialsFor(desktopId) {
212
- for (const { account } of await allCredentials()) {
213
- if (isBehindHost(account, desktopId)) {
214
- await forgetCredentials(account);
568
+ /**
569
+ * Forgets a desktop's saved passwords (see SavedFor; by default those behind any SSH host, for any gateway), from each
570
+ * store on its own. Where one can't be listed or won't forget now, its part goes later, and what it has of them isn't
571
+ * used meanwhile (see forgetLater): a user name changed while the Vault's prompt is cancelled doesn't leave the old
572
+ * one's password to sign in with, and a keychain that doesn't answer doesn't keep the Vault's from going at once.
573
+ * Returns the stores whose part waits (none, as a rule).
574
+ */
575
+ async function forgetCredentialsFor(desktopId, which = {}) {
576
+ const forgets = (key) => isFor(key, desktopId, which);
577
+ noteForget(forgets);
578
+ await used();
579
+ const waiting = [];
580
+ for (const inVault of [true, false]) {
581
+ const entries = await entriesOf(inVault);
582
+ if (!entries || !await forgetListed(inVault, entries, forgets)) {
583
+ forgetLater(forgets, inVault);
584
+ waiting.push(inVault ? 'vault' : 'keychain');
215
585
  }
216
586
  }
587
+ return waiting;
217
588
  }
218
- /** A desktop's address changed: its saved accounts (session keys ending in `#<from>`) move to the new address. */
219
- async function moveCredentialsFor(fromId, toId) {
220
- for (const { account, password } of await allCredentials()) {
221
- const credentials = parseCredentials(password);
222
- if (isBehindHost(account, fromId) && credentials) {
223
- try {
224
- await saveCredentials(`${account.slice(0, -fromId.length)}${toId}`, credentials);
225
- await forgetCredentials(account);
589
+ /**
590
+ * A desktop's address changed: its saved passwords (see SavedFor) move to the new address, for the same gateway. One
591
+ * that another desktop already has there (one at that address behind the same host, say) stays that desktop's: the
592
+ * moved one is dropped rather than put in its place, and the edited desktop asks. So is one for an address where a
593
+ * desktop is known already (`known`, by the session key there: a certificate is remembered for it, as it is when each
594
+ * password is looked at): the machine there may be another desktop's, which would otherwise get the edited desktop's
595
+ * password unasked, its certificate checking out as its own.
596
+ *
597
+ * Behind an SSH host, a password moves only with the certificate remembered for its desktop (`known` at the old
598
+ * address), which goes along and stays at the old address too (see RemoteDesktopService.desktopEdited): a desktop
599
+ * behind a host trusts its first certificate unasked, so a password that came to the new address alone would go to
600
+ * whatever answers there first, and so would one that stayed at the old address without it (another desktop's,
601
+ * reached another way). The keys don't show which host a desktop entry is behind, so an edit moves the passwords at
602
+ * that address behind every host: this way none of them reaches a machine at the new address that isn't the one it was
603
+ * saved for (another host's, or the one a mistyped address leads to) without a question. Without a certificate, a
604
+ * password is dropped, and that desktop asks. What isn't to be used (see withheld) doesn't move either. Each store's
605
+ * part is done on its own: one that can't be listed (a Vault whose prompt was cancelled, a keychain that doesn't
606
+ * answer) moves nothing, what it has at the old address goes later (see forgetLater), and what it has at the new one
607
+ * can't be seen, so only the other store's tells what is there already. Returns the stores whose part waits (none, as
608
+ * a rule): what they have at the old address is found there again should this window close first, where the
609
+ * certificate stays (see RemoteDesktopService.desktopEdited).
610
+ */
611
+ async function moveCredentialsFor(fromId, toId, which = {}, known = () => false) {
612
+ const forgets = (key) => isFor(key, fromId, which);
613
+ noteForget(forgets);
614
+ await used();
615
+ const stores = [];
616
+ for (const inVault of [true, false]) {
617
+ stores.push({ inVault, entries: await entriesOf(inVault) });
618
+ }
619
+ const taken = new Set(stores.flatMap(({ entries }) => entries ?? []).map(({ account }) => account));
620
+ const waiting = [];
621
+ for (const { inVault, entries } of stores) {
622
+ let done = !!entries;
623
+ for (const { account, password } of entries ?? []) {
624
+ const credentials = parseCredentials(password);
625
+ const { target, gateway } = (0, desktops_1.keyParts)(account);
626
+ if (forgets(account) && credentials) {
627
+ const to = `${target}#${toId}${gateway}`;
628
+ const pinned = target === desktops_1.DIRECT_KEY || known(`${target}#${fromId}`);
629
+ try {
630
+ if (!taken.has(to) && !known(`${target}#${toId}`) && pinned && !withheld(account, inVault)) {
631
+ await saveCredentials(to, credentials);
632
+ taken.add(to);
633
+ }
634
+ }
635
+ catch { }
636
+ try {
637
+ await forgetIn(inVault, account);
638
+ }
639
+ catch {
640
+ done = false;
641
+ }
226
642
  }
227
- catch { }
643
+ }
644
+ if (!done) {
645
+ forgetLater(forgets, inVault);
646
+ waiting.push(inVault ? 'vault' : 'keychain');
228
647
  }
229
648
  }
649
+ return waiting;
650
+ }
651
+ /**
652
+ * A target's key changed from `from`, a key no target has any more, whose saved passwords nothing moves (see
653
+ * RemoteTargets.keyChanged): those under it (its own desktop's and those of the desktops behind it, see rekeyed) are
654
+ * forgotten from each store once it can be read, and not used from it meanwhile (see forgetLater).
655
+ */
656
+ function forgetFormerKey(from) {
657
+ const forgets = (key) => (0, desktops_1.rekeyed)(key, from, from) !== null;
658
+ noteForget(forgets);
659
+ forgetLater(forgets, true);
660
+ forgetLater(forgets, false);
230
661
  }
231
662
  exports.STYLE = `
232
663
  .trd-signin { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1; }
@@ -297,4 +728,3 @@ function askCredentials(layer, options, abort) {
297
728
  abort.then(() => done(null));
298
729
  });
299
730
  }
300
- //# sourceMappingURL=signin.js.map