tabby-rdp 0.5.0 → 0.5.1

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 (79) hide show
  1. package/CHANGELOG.md +490 -0
  2. package/README.md +177 -48
  3. package/dist/accountForm.js +0 -1
  4. package/dist/accounts.js +0 -1
  5. package/dist/audio.js +0 -1
  6. package/dist/authorities.js +331 -0
  7. package/dist/byteQueue.js +102 -0
  8. package/dist/clipboard.js +106 -0
  9. package/dist/connectionStatus.js +0 -1
  10. package/dist/desk.js +16 -47
  11. package/dist/deskScript.js +196 -6
  12. package/dist/desktop.service.js +1442 -244
  13. package/dist/desktopForm.js +51 -17
  14. package/dist/desktops.js +232 -24
  15. package/dist/drives.js +271 -30
  16. package/dist/fileTransfer.js +958 -83
  17. package/dist/gateway.js +216 -57
  18. package/dist/header.js +76 -5
  19. package/dist/help.js +34 -13
  20. package/dist/hyperv.js +65 -15
  21. package/dist/index.js +2 -2
  22. package/dist/keyboard.js +0 -1
  23. package/dist/microphone.js +57 -13
  24. package/dist/ntlm.js +0 -1
  25. package/dist/osd.js +56 -15
  26. package/dist/probe.js +15 -3
  27. package/dist/rdcleanpath.js +319 -44
  28. package/dist/rdpFile.js +22 -7
  29. package/dist/rdpProfile.js +39 -21
  30. package/dist/remoteSetup.js +103 -23
  31. package/dist/sessionLog.js +53 -0
  32. package/dist/settingsPage.js +65 -29
  33. package/dist/signin.js +407 -40
  34. package/dist/ssh.js +99 -18
  35. package/dist/sshChannelStream.js +40 -6
  36. package/dist/targets.js +185 -39
  37. package/dist/ui.js +88 -20
  38. package/dist/unshowable.js +41 -0
  39. package/dist/updates.js +37 -2
  40. package/dist/vms.js +17 -3
  41. package/dist/wake.js +15 -8
  42. package/package.json +4 -3
  43. package/remote/trd-pty.py +74 -13
  44. package/vendor/SHA256SUMS +26 -0
  45. package/vendor/ironrdp_web_bg.wasm +0 -0
  46. package/dist/accountForm.js.map +0 -1
  47. package/dist/accounts.js.map +0 -1
  48. package/dist/audio.js.map +0 -1
  49. package/dist/connectionStatus.js.map +0 -1
  50. package/dist/desk.js.map +0 -1
  51. package/dist/deskScript.js.map +0 -1
  52. package/dist/desktop.service.js.map +0 -1
  53. package/dist/desktopForm.js.map +0 -1
  54. package/dist/desktops.js.map +0 -1
  55. package/dist/drives.js.map +0 -1
  56. package/dist/fileTransfer.js.map +0 -1
  57. package/dist/gateway.js.map +0 -1
  58. package/dist/header.js.map +0 -1
  59. package/dist/help.js.map +0 -1
  60. package/dist/hyperv.js.map +0 -1
  61. package/dist/index.js.map +0 -1
  62. package/dist/keyboard.js.map +0 -1
  63. package/dist/microphone.js.map +0 -1
  64. package/dist/ntlm.js.map +0 -1
  65. package/dist/osd.js.map +0 -1
  66. package/dist/probe.js.map +0 -1
  67. package/dist/rdcleanpath.js.map +0 -1
  68. package/dist/rdpFile.js.map +0 -1
  69. package/dist/rdpProfile.js.map +0 -1
  70. package/dist/remoteSetup.js.map +0 -1
  71. package/dist/settingsPage.js.map +0 -1
  72. package/dist/signin.js.map +0 -1
  73. package/dist/ssh.js.map +0 -1
  74. package/dist/sshChannelStream.js.map +0 -1
  75. package/dist/targets.js.map +0 -1
  76. package/dist/ui.js.map +0 -1
  77. package/dist/updates.js.map +0 -1
  78. package/dist/vms.js.map +0 -1
  79. package/dist/wake.js.map +0 -1
package/dist/signin.js CHANGED
@@ -3,13 +3,20 @@ 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.forgetMark = forgetMark;
12
+ exports.forgottenSince = forgottenSince;
9
13
  exports.forgetCredentials = forgetCredentials;
10
14
  exports.forgetCredentialsOrFail = forgetCredentialsOrFail;
15
+ exports.gatewayScope = gatewayScope;
16
+ exports.isFor = isFor;
11
17
  exports.forgetCredentialsFor = forgetCredentialsFor;
12
18
  exports.moveCredentialsFor = moveCredentialsFor;
19
+ exports.forgetFormerKey = forgetFormerKey;
13
20
  exports.askCredentials = askCredentials;
14
21
  /**
15
22
  * Sign-in for desktops that use their own accounts (Windows): a form in the desktop layer, and where passwords are
@@ -17,6 +24,7 @@ exports.askCredentials = askCredentials;
17
24
  * keychain.
18
25
  */
19
26
  const desktops_1 = require("./desktops");
27
+ const gateway_1 = require("./gateway");
20
28
  const KEYCHAIN_SERVICE = 'tabby-rdp';
21
29
  /** The Vault secret type of a desktop's or account's password; its key is `{ key, name }` (see vaultKey). */
22
30
  exports.VAULT_SECRET_TYPE = 'rdp:password';
@@ -52,8 +60,9 @@ function vaultKey(key) {
52
60
  return { key };
53
61
  }
54
62
  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;
63
+ const { target, desktopId, gateway } = (0, desktops_1.keyParts)(key);
64
+ const where = `${desktopId}${gateway ? gateway.replace('@gateway#', ' through gateway ') : ''}`;
65
+ return target === 'account' ? `saved account` : target === desktops_1.DIRECT_KEY ? `${where} (direct)` : desktopId ? `${where} behind ${target}` : target;
57
66
  }
58
67
  // The system keychain can keep a call waiting indefinitely: on Linux, a locked or missing keyring waits for an unlock
59
68
  // prompt, which a session without a keyring prompter never shows. Give up after a while. A call that never returns
@@ -105,12 +114,196 @@ function parseCredentials(saved) {
105
114
  return null;
106
115
  }
107
116
  }
117
+ let once = null;
118
+ let onceRunning = null;
119
+ /**
120
+ * Forgets the saved passwords whose key `forgets` matches, once in this computer's keychain and once in the Vault,
121
+ * before the store is next used: when it is asked anyway, so that a locked Vault asks for its passphrase at a sign-in
122
+ * rather than when Tabby starts. Each part's mark is read as that part is about to be done (another Tabby window may
123
+ * 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
124
+ * it holds; the Vault's prompt is cancelled) is tried again at the next use, and until it is done, what it would
125
+ * forget isn't looked up in that store (see withheld): the desktop asks.
126
+ */
127
+ function forgetOnce(mark, forgets, vault) {
128
+ once = { mark, forgets, vault, keychainDone: false };
129
+ }
130
+ /** Does what forgetOnce and forgetLater left to do, if anything, and waits for it to end. */
131
+ async function used() {
132
+ if (once && !onceRunning) {
133
+ const job = once;
134
+ onceRunning = doOnce(job).catch(() => undefined).finally(() => { onceRunning = null; });
135
+ }
136
+ await onceRunning;
137
+ if (later.length && !laterRunning) {
138
+ laterRunning = doLater().catch(() => undefined).finally(() => { laterRunning = null; });
139
+ }
140
+ await laterRunning;
141
+ }
142
+ /** 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. */
143
+ async function doOnce(job) {
144
+ let forgets;
145
+ try {
146
+ forgets = job.forgets();
147
+ }
148
+ catch {
149
+ return;
150
+ }
151
+ if (!job.keychainDone) {
152
+ try {
153
+ job.keychainDone = await keychainOnce(job.mark, forgets);
154
+ }
155
+ catch { }
156
+ }
157
+ if (!job.vault.done()) {
158
+ try {
159
+ if (vaultOn()) {
160
+ // Read in full or not at all: a passphrase prompt cancelled throws, and the part is tried again.
161
+ const contents = await vault.load();
162
+ for (const secret of contents?.secrets ?? []) {
163
+ if (secret.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string' && forgets(secret.key.key)) {
164
+ await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(secret.key.key)));
165
+ }
166
+ }
167
+ }
168
+ // With the Vault off there is nothing in it: Tabby drops its contents when it is turned off.
169
+ job.vault.markDone();
170
+ }
171
+ catch { }
172
+ }
173
+ if (job.keychainDone && job.vault.done() && once === job) {
174
+ once = null;
175
+ }
176
+ }
177
+ /**
178
+ * This computer's keychain's part of forgetOnce: done before (its mark is there), or now, and marked. True without a
179
+ * keychain, where there is nothing to do. Throws when the keychain doesn't answer, or can't list what it holds: a
180
+ * part done on a listing that came back empty because of that would leave what it should have forgotten in place.
181
+ */
182
+ async function keychainOnce(mark, forgets) {
183
+ const marked = await keychain(store => store.getPassword(KEYCHAIN_SERVICE, mark));
184
+ if (marked === undefined || marked) {
185
+ return true;
186
+ }
187
+ const entries = await keychain(store => store.findCredentials(KEYCHAIN_SERVICE));
188
+ for (const { account } of entries ?? []) {
189
+ if (typeof account === 'string' && account !== mark && forgets(account)) {
190
+ await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, account));
191
+ }
192
+ }
193
+ await keychain(store => store.setPassword(KEYCHAIN_SERVICE, mark, 'done'));
194
+ return true;
195
+ }
196
+ /**
197
+ * Forgets that couldn't be done when they were asked for (see forgetLater), each of one store's part: the Vault's
198
+ * (`inVault`), or the keychain's. Each is done the next time its store is used and can be read (see used, doLater),
199
+ * and what it would forget isn't looked up in that store meanwhile (see withheld). `saved`: keys a password was saved
200
+ * 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.
201
+ */
202
+ const later = [];
203
+ let laterRunning = null;
204
+ /**
205
+ * The saved passwords whose key `forgets` matches are to go from one store (the Vault, or with `inVault` false the
206
+ * keychain), and couldn't now (it couldn't be listed or wouldn't forget: a Vault whose passphrase prompt was
207
+ * cancelled, a keychain that didn't answer): forgotten once that store can be read (see doLater), and not used from it
208
+ * meanwhile, so that the desktop asks. The other store's part doesn't wait for it.
209
+ */
210
+ function forgetLater(forgets, inVault) {
211
+ later.push({ forgets, inVault, saved: new Set() });
212
+ }
213
+ /**
214
+ * 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
215
+ * keychain's whenever it can be listed; the Vault's at once while the Vault is off (there is nothing in it then: Tabby
216
+ * drops its contents when it is turned off), and otherwise once it is open. Not at the cost of a passphrase prompt of
217
+ * its own: with the Vault locked, its part waits for the Vault's next unlock (by whatever unlocks it, a password looked
218
+ * up or saved), and what it covers isn't used meanwhile.
219
+ */
220
+ async function doLater() {
221
+ for (const inVault of [true, false]) {
222
+ const jobs = later.filter(job => job.inVault === inVault);
223
+ if (!jobs.length || inVault && vaultOn() && !vaultOpen()) {
224
+ continue;
225
+ }
226
+ const entries = await entriesOf(inVault);
227
+ if (!entries) {
228
+ continue;
229
+ }
230
+ for (const job of jobs) {
231
+ if (await forgetListed(inVault, entries, job.forgets, job.saved)) {
232
+ later.splice(later.indexOf(job), 1);
233
+ }
234
+ }
235
+ }
236
+ }
237
+ function vaultOpen() {
238
+ try {
239
+ return !!vault?.isOpen();
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ /**
246
+ * Whether `key` is one forgetOnce's work would forget from that store (the Vault, or with `inVault` false the
247
+ * keychain), and that part isn't done yet (see used), or one a forget left for later would forget from that store (see
248
+ * forgetLater): what is saved under it there isn't used meanwhile.
249
+ */
250
+ function withheld(key, inVault) {
251
+ if (later.some(job => job.inVault === inVault && !job.saved.has(key) && job.forgets(key))) {
252
+ return true;
253
+ }
254
+ const job = once;
255
+ if (!job || (inVault ? job.vault.done() : job.keychainDone)) {
256
+ return false;
257
+ }
258
+ try {
259
+ return job.forgets()(key);
260
+ }
261
+ catch {
262
+ return true;
263
+ }
264
+ }
265
+ /**
266
+ * Targets whose key changed (see RemoteTargets.keyChanged): the key each had up to 0.5.0, by the key it has now. A
267
+ * password saved under the former one moves to the new one the first time it is looked for there (see loadCredentials):
268
+ * when its store is asked anyway, so a locked Vault asks for its passphrase at a sign-in rather than when a tab opens.
269
+ */
270
+ const formerKeys = new Map();
271
+ /** A target's key changed from `from` to `to` (see formerKeys). */
272
+ function renameCredentials(from, to) {
273
+ if (from !== to) {
274
+ formerKeys.set(to, from);
275
+ }
276
+ }
108
277
  /**
109
278
  * 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.
279
+ * on, the keychain is still read for what was saved before it was turned on. Credentials saved under the target's
280
+ * former key (see formerKeys) are moved under this one, and returned.
111
281
  */
112
282
  async function loadCredentials(key) {
113
- if (vaultOn()) {
283
+ await used();
284
+ const found = await loadSaved(key);
285
+ if (found) {
286
+ return found;
287
+ }
288
+ for (const [to, from] of formerKeys) {
289
+ const former = (0, desktops_1.rekeyed)(key, to, from);
290
+ if (former) {
291
+ const moved = await loadSaved(former);
292
+ if (moved) {
293
+ // Forgotten there only once saved here: should the store refuse, it is found there again next time.
294
+ try {
295
+ await saveCredentials(key, moved);
296
+ await forgetCredentialsOrFail(former);
297
+ }
298
+ catch { }
299
+ }
300
+ return moved;
301
+ }
302
+ }
303
+ return null;
304
+ }
305
+ async function loadSaved(key) {
306
+ if (vaultOn() && !withheld(key, true)) {
114
307
  try {
115
308
  const secret = await vault.getSecret(exports.VAULT_SECRET_TYPE, vaultKey(key));
116
309
  const found = parseCredentials(secret?.value);
@@ -120,6 +313,9 @@ async function loadCredentials(key) {
120
313
  }
121
314
  catch { }
122
315
  }
316
+ if (withheld(key, false)) {
317
+ return null;
318
+ }
123
319
  try {
124
320
  return parseCredentials(await keychain(store => store.getPassword(KEYCHAIN_SERVICE, key)));
125
321
  }
@@ -158,8 +354,10 @@ async function hasCredentials(key) {
158
354
  }
159
355
  }
160
356
  async function saveCredentials(key, credentials, label) {
357
+ await used();
161
358
  if (vaultOn()) {
162
359
  await queued(() => vault.addSecret({ type: exports.VAULT_SECRET_TYPE, key: { ...vaultKey(key), description: label ?? vaultName(key) }, value: JSON.stringify(credentials) }));
360
+ newlySaved(key, true);
163
361
  // Not in two places: what the keychain had for it goes.
164
362
  try {
165
363
  await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, key));
@@ -168,6 +366,50 @@ async function saveCredentials(key, credentials, label) {
168
366
  return;
169
367
  }
170
368
  await keychain(store => store.setPassword(KEYCHAIN_SERVICE, key, JSON.stringify(credentials)));
369
+ newlySaved(key, false);
370
+ }
371
+ /**
372
+ * A password was saved under `key` just now, in the Vault or with `inVault` false the keychain, in place of what a
373
+ * forget left for later would forget from that store (see forgetLater): it stays. (What the other store has under that
374
+ * 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.
375
+ */
376
+ function newlySaved(key, inVault) {
377
+ for (const job of later) {
378
+ if (job.inVault === inVault) {
379
+ job.saved.add(key);
380
+ }
381
+ }
382
+ if (later.length) {
383
+ used().catch(() => undefined);
384
+ }
385
+ }
386
+ /**
387
+ * The latest forgets (a password forgotten for a key, an edit or a removal of a desktop, the passwords under a former
388
+ * key), each with a number and what it covers. A sign-in that began before one and ends after it, the server having
389
+ * taken its time, mustn't save its password again, which would undo the forget (see forgottenSince). Only the last
390
+ * FORGETS_KEPT are kept.
391
+ */
392
+ const FORGETS_KEPT = 256;
393
+ const forgotten = [];
394
+ let forgetCount = 0;
395
+ /** Records a forget, as it is asked for: what began before it doesn't save under the keys it covers. */
396
+ function noteForget(covers) {
397
+ forgotten.push({ n: ++forgetCount, covers });
398
+ if (forgotten.length > FORGETS_KEPT) {
399
+ forgotten.shift();
400
+ }
401
+ }
402
+ /** Where the forgets stand now: what a sign-in takes when its credentials are, to ask forgottenSince when it ends. */
403
+ function forgetMark() {
404
+ return forgetCount;
405
+ }
406
+ /**
407
+ * Whether a forget covering `key` was asked for since `mark` (see forgetMark): the desktop was edited or removed, or
408
+ * what was saved for it forgotten, while a sign-in ended later was under way. A mark older than the forgets kept counts
409
+ * as forgotten since: a sign-in that slow asks again, at worst.
410
+ */
411
+ function forgottenSince(key, mark) {
412
+ return forgetCount - mark > forgotten.length || forgotten.some(f => f.n > mark && f.covers(key));
171
413
  }
172
414
  async function forgetCredentials(key) {
173
415
  try {
@@ -177,56 +419,182 @@ async function forgetCredentials(key) {
177
419
  }
178
420
  /** As forgetCredentials, but a store that wouldn't (a Vault prompt cancelled, a keychain not answering) is reported. */
179
421
  async function forgetCredentialsOrFail(key) {
180
- if (vaultOn()) {
181
- await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key)));
422
+ noteForget(k => k === key);
423
+ await used();
424
+ return forgetNow(key);
425
+ }
426
+ /** Forgets what is saved under `key`, in both stores. */
427
+ async function forgetNow(key) {
428
+ await forgetIn(true, key);
429
+ await forgetIn(false, key);
430
+ }
431
+ /** Forgets what is saved under `key` in one store: the Vault (while it is on), or with `inVault` false the keychain. */
432
+ async function forgetIn(inVault, key) {
433
+ if (inVault) {
434
+ if (vaultOn()) {
435
+ await queued(() => vault.removeSecret(exports.VAULT_SECRET_TYPE, vaultKey(key)));
436
+ }
437
+ return;
182
438
  }
183
439
  // No keychain at all is not a failure to forget; one that didn't answer is.
184
440
  await keychain(store => store.deletePassword(KEYCHAIN_SERVICE, key));
185
441
  }
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
- }
442
+ /**
443
+ * Every entry saved in one store, by its key: the Vault's (none while it is off: Tabby drops its contents when it is
444
+ * turned off), or with `inVault` false the keychain's (none without one). Null where that store can't be listed in full
445
+ * now (a Vault whose passphrase prompt was cancelled, a keychain that doesn't answer): what goes by such a list,
446
+ * forgetting or moving a desktop's passwords, would leave the rest in place, and in use. Each store is listed on its
447
+ * own, so that one that can't be doesn't keep what the other holds from being done.
448
+ */
449
+ async function entriesOf(inVault) {
450
+ try {
451
+ if (!inVault) {
452
+ const entries = await keychain(store => store.findCredentials(KEYCHAIN_SERVICE));
453
+ return (entries ?? []).filter(entry => typeof entry?.account === 'string');
197
454
  }
198
- catch { }
455
+ if (!vaultOn()) {
456
+ return [];
457
+ }
458
+ const contents = await vault.load();
459
+ return (contents?.secrets ?? []).filter((secret) => secret?.type === exports.VAULT_SECRET_TYPE && typeof secret.key?.key === 'string')
460
+ .map((secret) => ({ account: secret.key.key, password: secret.value }));
199
461
  }
200
- try {
201
- entries.push(...await keychain(store => store.findCredentials(KEYCHAIN_SERVICE)) ?? []);
462
+ catch {
463
+ return null;
202
464
  }
203
- catch { }
204
- return entries;
205
465
  }
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}#`);
466
+ /**
467
+ * Forgets from one store (the Vault, or with `inVault` false the keychain) what `entries`, all it holds (see
468
+ * entriesOf), has under a key `forgets` matches, but for those `kept`. False where it wouldn't forget one of them.
469
+ */
470
+ async function forgetListed(inVault, entries, forgets, kept) {
471
+ let done = true;
472
+ for (const key of new Set(entries.map(({ account }) => account))) {
473
+ if (!kept?.has(key) && forgets(key)) {
474
+ try {
475
+ await forgetIn(inVault, key);
476
+ }
477
+ catch {
478
+ done = false;
479
+ }
480
+ }
481
+ }
482
+ return done;
483
+ }
484
+ /**
485
+ * What a saved desktop password's key adds for the RD Gateway it goes through (`@gateway#host:port`, the gateway as
486
+ * parseGateway has it, so another spelling of it is the same one), or '' for none. Whenever a desktop has a gateway,
487
+ * its password is kept for that gateway too, a gateway account of its own or not: a password saved for one gateway
488
+ * (or for a connection without one) is never sent through a different gateway named for the same address, which could
489
+ * lead somewhere else, without asking.
490
+ */
491
+ function gatewayScope(gateway) {
492
+ const parsed = gateway ? (0, gateway_1.parseGateway)(gateway) : null;
493
+ return parsed ? `@gateway#${parsed.host}:${parsed.port}` : '';
494
+ }
495
+ /**
496
+ * Whether a credential key is one of a desktop's saved passwords (see SavedFor). Anchored on the key's parts, so an
497
+ * unrelated key that merely ends in `#<id>` (a gateway pin, or a desktop whose own id is a suffix of another's) isn't
498
+ * matched; a gateway suffix on the key doesn't change the id.
499
+ */
500
+ function isFor(account, desktopId, which) {
501
+ const { target, desktopId: id, gateway } = (0, desktops_1.keyParts)(account);
502
+ return id === desktopId && (which.direct ? target === desktops_1.DIRECT_KEY : target !== desktops_1.DIRECT_KEY && target !== 'gateway' && target !== 'account') &&
503
+ (which.scope === undefined || gateway === which.scope);
209
504
  }
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);
505
+ /**
506
+ * Forgets a desktop's saved passwords (see SavedFor; by default those behind any SSH host, for any gateway), from each
507
+ * 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
508
+ * used meanwhile (see forgetLater): a user name changed while the Vault's prompt is cancelled doesn't leave the old
509
+ * one's password to sign in with, and a keychain that doesn't answer doesn't keep the Vault's from going at once.
510
+ * Returns the stores whose part waits (none, as a rule).
511
+ */
512
+ async function forgetCredentialsFor(desktopId, which = {}) {
513
+ const forgets = (key) => isFor(key, desktopId, which);
514
+ noteForget(forgets);
515
+ await used();
516
+ const waiting = [];
517
+ for (const inVault of [true, false]) {
518
+ const entries = await entriesOf(inVault);
519
+ if (!entries || !await forgetListed(inVault, entries, forgets)) {
520
+ forgetLater(forgets, inVault);
521
+ waiting.push(inVault ? 'vault' : 'keychain');
215
522
  }
216
523
  }
524
+ return waiting;
217
525
  }
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);
526
+ /**
527
+ * A desktop's address changed: its saved passwords (see SavedFor) move to the new address, for the same gateway. One
528
+ * that another desktop already has there (one at that address behind the same host, say) stays that desktop's: the
529
+ * moved one is dropped rather than put in its place, and the edited desktop asks. So is one for an address where a
530
+ * desktop is known already (`known`, by the session key there: a certificate is remembered for it, as it is when each
531
+ * password is looked at): the machine there may be another desktop's, which would otherwise get the edited desktop's
532
+ * password unasked, its certificate checking out as its own.
533
+ *
534
+ * Behind an SSH host, a password moves only with the certificate remembered for its desktop (`known` at the old
535
+ * address), which goes along and stays at the old address too (see RemoteDesktopService.desktopEdited): a desktop
536
+ * behind a host trusts its first certificate unasked, so a password that came to the new address alone would go to
537
+ * whatever answers there first, and so would one that stayed at the old address without it (another desktop's,
538
+ * reached another way). The keys don't show which host a desktop entry is behind, so an edit moves the passwords at
539
+ * that address behind every host: this way none of them reaches a machine at the new address that isn't the one it was
540
+ * saved for (another host's, or the one a mistyped address leads to) without a question. Without a certificate, a
541
+ * password is dropped, and that desktop asks. What isn't to be used (see withheld) doesn't move either. Each store's
542
+ * part is done on its own: one that can't be listed (a Vault whose prompt was cancelled, a keychain that doesn't
543
+ * answer) moves nothing, what it has at the old address goes later (see forgetLater), and what it has at the new one
544
+ * can't be seen, so only the other store's tells what is there already. Returns the stores whose part waits (none, as
545
+ * a rule): what they have at the old address is found there again should this window close first, where the
546
+ * certificate stays (see RemoteDesktopService.desktopEdited).
547
+ */
548
+ async function moveCredentialsFor(fromId, toId, which = {}, known = () => false) {
549
+ const forgets = (key) => isFor(key, fromId, which);
550
+ noteForget(forgets);
551
+ await used();
552
+ const stores = [];
553
+ for (const inVault of [true, false]) {
554
+ stores.push({ inVault, entries: await entriesOf(inVault) });
555
+ }
556
+ const taken = new Set(stores.flatMap(({ entries }) => entries ?? []).map(({ account }) => account));
557
+ const waiting = [];
558
+ for (const { inVault, entries } of stores) {
559
+ let done = !!entries;
560
+ for (const { account, password } of entries ?? []) {
561
+ const credentials = parseCredentials(password);
562
+ const { target, gateway } = (0, desktops_1.keyParts)(account);
563
+ if (forgets(account) && credentials) {
564
+ const to = `${target}#${toId}${gateway}`;
565
+ const pinned = target === desktops_1.DIRECT_KEY || known(`${target}#${fromId}`);
566
+ try {
567
+ if (!taken.has(to) && !known(`${target}#${toId}`) && pinned && !withheld(account, inVault)) {
568
+ await saveCredentials(to, credentials);
569
+ taken.add(to);
570
+ }
571
+ }
572
+ catch { }
573
+ try {
574
+ await forgetIn(inVault, account);
575
+ }
576
+ catch {
577
+ done = false;
578
+ }
226
579
  }
227
- catch { }
580
+ }
581
+ if (!done) {
582
+ forgetLater(forgets, inVault);
583
+ waiting.push(inVault ? 'vault' : 'keychain');
228
584
  }
229
585
  }
586
+ return waiting;
587
+ }
588
+ /**
589
+ * A target's key changed from `from`, a key no target has any more, whose saved passwords nothing moves (see
590
+ * RemoteTargets.keyChanged): those under it (its own desktop's and those of the desktops behind it, see rekeyed) are
591
+ * forgotten from each store once it can be read, and not used from it meanwhile (see forgetLater).
592
+ */
593
+ function forgetFormerKey(from) {
594
+ const forgets = (key) => (0, desktops_1.rekeyed)(key, from, from) !== null;
595
+ noteForget(forgets);
596
+ forgetLater(forgets, true);
597
+ forgetLater(forgets, false);
230
598
  }
231
599
  exports.STYLE = `
232
600
  .trd-signin { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1; }
@@ -297,4 +665,3 @@ function askCredentials(layer, options, abort) {
297
665
  abort.then(() => done(null));
298
666
  });
299
667
  }
300
- //# sourceMappingURL=signin.js.map