sliftutils 1.7.126 → 1.7.127

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.
@@ -8,7 +8,7 @@ import { deriveRevokeKey, REVOKE_KEY_LABEL, revokeRepoURL } from "./revokeSource
8
8
  import { revokedKeysInRepo } from "./unrevoke";
9
9
  import { expandHome } from "../helpers/paths";
10
10
  import { spawnPromise } from "../helpers/spawn";
11
- import { readRemoteFile, remoteCommandExists, runOverSSH, SUDO_PREAMBLE, writeRemoteFile } from "../helpers/remoteSSH";
11
+ import { describeHost, readRemoteFile, remoteCommandExists, runOverSSH, SUDO_PREAMBLE, THIS_MACHINE, writeRemoteFile } from "../helpers/remoteSSH";
12
12
 
13
13
  const SERVICE_SOURCE = path.join(__dirname, "daemon", "portsecure.service");
14
14
  // The daemon runs from a checkout of this repo on the host, rather than from a copy we upload, so
@@ -22,12 +22,16 @@ const ROOT_AUTHORIZED_KEYS = "/root/.ssh/authorized_keys";
22
22
  const SERVICE_NAME = "portsecure";
23
23
  const MAX_ERROR_BODY_LENGTH = 500;
24
24
  const VERBS = ["add", "remove", "list", "update"];
25
- // The repo url is optional, and defaults to the repo the command is run from.
25
+ // The host is optional, and without one everything happens on this machine. The repo url is
26
+ // optional too, and defaults to the repo the command is run from.
26
27
  const USAGE = `Usage:
27
- yarn securessh <host> add <repo-private-key> [repo-url]
28
- yarn securessh <host> remove [repo-url]
29
- yarn securessh <host> list
30
- yarn securessh <host> update`;
28
+ yarn securessh [host] add <repo-private-key> [repo-url]
29
+ yarn securessh [host] remove [repo-url]
30
+ yarn securessh [host] list
31
+ yarn securessh [host] update
32
+
33
+ With no host it acts on this machine, and still installs from github rather than from wherever
34
+ this was run.`;
31
35
 
32
36
  async function pathExists(filePath: string) {
33
37
  try {
@@ -176,7 +180,19 @@ async function ensureRevokeReposExist(repoSources: string[]) {
176
180
  let revokeURL = revokeRepoURL(repoURL);
177
181
  let temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "portsecure-revoke-"));
178
182
  let checkoutPath = path.join(temporaryDirectory, "repo");
179
- let clone = await runLocal({ command: "git", args: ["clone", revokeURL, checkoutPath], allowFailure: true });
183
+ // On a machine that is already a portsecure host, the source's deploy key is right there,
184
+ // and the key derived from it is the one credential that repo is guaranteed to accept.
185
+ // Anywhere else, a person is running this and has their own access.
186
+ let ownKey = "";
187
+ if (await pathExists(sourceKeyPath(repoURL))) {
188
+ let derived = deriveRevokeKey(await fs.readFile(sourceKeyPath(repoURL), "utf8"));
189
+ ownKey = path.join(temporaryDirectory, "key");
190
+ await fs.writeFile(ownKey, derived.privateKeyFile, { mode: 0o600 });
191
+ }
192
+ let cloneRevoke = async (allowFailure: boolean) => ownKey
193
+ && await gitWithKey({ keyPath: ownKey, args: ["clone", revokeURL, checkoutPath], allowFailure })
194
+ || await runLocal({ command: "git", args: ["clone", revokeURL, checkoutPath], allowFailure });
195
+ let clone = await cloneRevoke(true);
180
196
  if (clone.status !== 0) {
181
197
  await fs.rm(temporaryDirectory, { recursive: true, force: true });
182
198
  throw new Error(
@@ -194,10 +210,15 @@ async function ensureRevokeReposExist(repoSources: string[]) {
194
210
  for (let args of [
195
211
  ["-C", checkoutPath, "add", "-A"],
196
212
  ["-C", checkoutPath, "-c", "user.email=portsecure@localhost", "-c", "user.name=portsecure", "commit", "-m", "initialise revoke repo"],
197
- ["-C", checkoutPath, "push", "origin", "HEAD"],
198
213
  ]) {
199
214
  await runLocal({ command: "git", args });
200
215
  }
216
+ let push = ["-C", checkoutPath, "push", "origin", "HEAD"];
217
+ if (ownKey) {
218
+ await gitWithKey({ keyPath: ownKey, args: push });
219
+ } else {
220
+ await runLocal({ command: "git", args: push });
221
+ }
201
222
  }
202
223
  await fs.rm(temporaryDirectory, { recursive: true, force: true });
203
224
  console.log(`${revokeURL} is ready`);
@@ -296,14 +317,24 @@ async function installDaemon(config: { host: string; hostLabel: string; repoSour
296
317
 
297
318
  // The checkout is brought to the latest commit rather than a copy being pushed, so what runs on
298
319
  // the host is exactly what is on github.
299
- console.log(`Updating ${REMOTE_CHECKOUT_PATH} on ${host}`);
320
+ console.log(`Updating ${REMOTE_CHECKOUT_PATH} on ${describeHost(host)}`);
300
321
  await runOverSSH({
301
322
  host,
302
323
  script: `${SUDO_PREAMBLE}
303
324
  set -e
304
325
  $SUDO mkdir -p "${path.posix.dirname(REMOTE_CHECKOUT_PATH)}"
305
326
  if $SUDO test -d "${REMOTE_CHECKOUT_PATH}/.git"; then
327
+ # Stashed first, the way machine-alwaysup does it. Anything sitting modified in a host's
328
+ # checkout is either an accident or somebody editing the daemon in place, and either way it is
329
+ # worth keeping rather than quietly erasing. The identity is passed inline because a host has
330
+ # no git config of its own and stash writes a commit.
331
+ $SUDO git -C "${REMOTE_CHECKOUT_PATH}" add --all
332
+ $SUDO git -C "${REMOTE_CHECKOUT_PATH}" -c user.email=portsecure@localhost -c user.name=portsecure stash
306
333
  $SUDO git -C "${REMOTE_CHECKOUT_PATH}" fetch --prune origin
334
+ # A stash and a pull cannot cross a branch that has diverged, and a drifted checkout must not be
335
+ # able to leave a host running old code, so it is put on the remote's state rather than merged
336
+ # with it. set-head so that is the remote's default branch now, not the one it had at clone.
337
+ $SUDO git -C "${REMOTE_CHECKOUT_PATH}" remote set-head origin --auto
307
338
  $SUDO git -C "${REMOTE_CHECKOUT_PATH}" reset --hard origin/HEAD
308
339
  else
309
340
  $SUDO rm -rf "${REMOTE_CHECKOUT_PATH}"
@@ -368,7 +399,7 @@ async function requireRemoteWebhook(host: string) {
368
399
  throw new Error(
369
400
  `Expected a Discord webhook at ${DEFAULT_WEBHOOK_FILE_PATH} on ${host}, no such file exists.`
370
401
  + ` The daemon will not start without one.\n`
371
- + `Set it up first:\n yarn setupnotify ${host} <discord-webhook-url>`
402
+ + `Set it up first:\n yarn setupnotify ${host} <discord-webhook-url>`.replace(" <discord", host && " <discord" || "<discord")
372
403
  );
373
404
  }
374
405
  return parseWebhookFile({ contents, sourceName: `${host}:${DEFAULT_WEBHOOK_FILE_PATH}` });
@@ -392,18 +423,22 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
392
423
 
393
424
  let remoteConfig = await readRemoteConfig(host);
394
425
  if (remoteConfig.repoSources.includes(repoURL)) {
395
- console.log(`${host} already has ${repoURL}, refreshing its key and the daemon.`);
426
+ console.log(`${describeHost(host)} already has ${repoURL}, refreshing its key and the daemon.`);
396
427
  }
397
428
 
398
- // The merged result is what root ends up with, so our own key has to be somewhere in it.
399
- console.log(`Checking our access to ${host} survives the merged keys`);
400
- let ourFingerprint = await findAuthenticatingFingerprint(host);
429
+ // The merged result is what root ends up with, so our own key has to be somewhere in it. On
430
+ // this machine there is no ssh session to preserve, so there is nothing to check.
431
+ let ourFingerprint = "";
432
+ if (host) {
433
+ console.log(`Checking our access to ${describeHost(host)} survives the merged keys`);
434
+ ourFingerprint = await findAuthenticatingFingerprint(host);
435
+ }
401
436
  let inspectionPath = await cloneRepoForInspection({ repoURL, keyPath });
402
437
  let newKeys = await readRepoKeys(inspectionPath);
403
438
  // Whatever is applied on the host came from the existing sources, so it stays in the merge.
404
439
  let existingKeys = normalizeKeys(await readRemoteFile({ host, filePath: ROOT_AUTHORIZED_KEYS }) || "");
405
440
  let mergedFingerprints = await fingerprintKeys([...existingKeys, ...newKeys]);
406
- if (!mergedFingerprints.includes(ourFingerprint)) {
441
+ if (ourFingerprint && !mergedFingerprints.includes(ourFingerprint)) {
407
442
  throw new Error(
408
443
  `Expected the key we use for ${host} to be in the merged keys, it is not.\n`
409
444
  + `Ours: ${ourFingerprint}\n`
@@ -412,7 +447,9 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
412
447
  + ` lock you out of ${host}. Add your public key to ${repoURL} first.`
413
448
  );
414
449
  }
415
- console.log(`Our key ${ourFingerprint} is in the merged keys, access will survive.`);
450
+ if (ourFingerprint) {
451
+ console.log(`Our key ${ourFingerprint} is in the merged keys, access will survive.`);
452
+ }
416
453
 
417
454
  await ensureRevokeRepo({ keyPath, repoURL });
418
455
 
@@ -428,7 +465,7 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
428
465
  }
429
466
 
430
467
  let webhookURL = await requireRemoteWebhook(host);
431
- console.log(`${host} notifies ${webhookURL}`);
468
+ console.log(`${describeHost(host)} notifies ${webhookURL}`);
432
469
 
433
470
  await writeRemoteFile({
434
471
  host,
@@ -441,7 +478,7 @@ async function addSource(config: { host: string; keyPath: string; repoURL: strin
441
478
  let repoSources = remoteConfig.repoSources.filter(source => source !== repoURL);
442
479
  repoSources.push(repoURL);
443
480
  await installDaemon({ host, hostLabel: remoteConfig.hostLabel, repoSources });
444
- console.log(`${repoURL} added to ${host}. ${repoSources.length} source(s) now merged.`);
481
+ console.log(`${repoURL} added to ${describeHost(host)}. ${repoSources.length} source(s) now merged.`);
445
482
  }
446
483
 
447
484
  async function removeSource(config: { host: string; repoURL: string }) {
@@ -455,9 +492,10 @@ async function removeSource(config: { host: string; repoURL: string }) {
455
492
  }
456
493
  let repoSources = remoteConfig.repoSources.filter(source => source !== repoURL);
457
494
 
458
- if (repoSources.length) {
495
+ // On this machine there is no ssh session to preserve, so there is nothing to check.
496
+ if (repoSources.length && host) {
459
497
  // The keys left over are what root gets, so our own key has to be among them.
460
- console.log(`Checking our access to ${host} survives without ${repoURL}`);
498
+ console.log(`Checking our access to ${describeHost(host)} survives without ${repoURL}`);
461
499
  let ourFingerprint = await findAuthenticatingFingerprint(host);
462
500
  let remainingKeys: string[] = [];
463
501
  for (let source of repoSources) {
@@ -485,47 +523,15 @@ $SUDO rm -f "${sourceKeyPath(repoURL)}"
485
523
  $SUDO rm -rf "${sourceRepoPath(repoURL)}"`,
486
524
  });
487
525
  await installDaemon({ host, hostLabel: remoteConfig.hostLabel, repoSources });
488
- console.log(`${repoURL} removed from ${host}. ${repoSources.length} source(s) left.`);
526
+ console.log(`${repoURL} removed from ${describeHost(host)}. ${repoSources.length} source(s) left.`);
489
527
  }
490
528
 
491
529
  /** Answers "who can log into this box, and which repo says so". The paths the daemon uses are
492
530
  left out on purpose, they are plumbing rather than something to act on. */
493
- /** The daemon is installed from this checkout, so the checkout is brought up to date first.
494
- Anything that stops the pull - a conflict, local commits, no upstream - stops the update too,
495
- rather than quietly installing whatever happened to be on disk. */
496
- async function pullLocalCheckout() {
497
- let topLevel = await spawnPromise({ command: "git", args: ["rev-parse", "--show-toplevel"], cwd: __dirname });
498
- if (topLevel.status !== 0) {
499
- throw new Error(
500
- `Expected ${__dirname} to be inside a git checkout, it is not.`
501
- + ` update installs the daemon from this checkout, so it has to be one.`
502
- );
503
- }
504
- let repoPath = topLevel.stdout.trim();
505
- let pull = await spawnPromise({ command: "git", args: ["pull", "--ff-only"], cwd: repoPath });
506
- if (pull.status !== 0) {
507
- throw new Error(
508
- `Expected git pull in ${repoPath} to succeed, it did not, so nothing was installed.\n`
509
- + `${(pull.stdout + pull.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
510
- );
511
- }
512
- // The host installs from github, not from here, so local commits that have not been pushed
513
- // are not what it will run.
514
- let local = await spawnPromise({ command: "git", args: ["rev-parse", "HEAD"], cwd: repoPath });
515
- let remote = await spawnPromise({ command: "git", args: ["rev-parse", "@{u}"], cwd: repoPath });
516
- if (local.stdout.trim() !== remote.stdout.trim()) {
517
- console.log(`WARNING: ${repoPath} has commits that are not pushed. The host installs from github,`);
518
- console.log(` so it will run the pushed version, not what is here.`);
519
- }
520
- console.log(`Pulled ${repoPath}`);
521
- }
522
531
 
523
532
  /** Pushes the current daemon onto a host that already has one, for when this code has moved on.
524
533
  Nothing about which keys the host trusts is touched. */
525
534
  async function updateDaemon(host: string) {
526
- // Pulled before anything else, so a checkout that cannot be brought up to date fails here
527
- // rather than after we have already started changing the host.
528
- await pullLocalCheckout();
529
535
  let contents = await readRemoteFile({ host, filePath: REMOTE_CONFIG_PATH });
530
536
  if (!contents) {
531
537
  throw new Error(
@@ -538,16 +544,21 @@ async function updateDaemon(host: string) {
538
544
  await requireRemoteWebhook(host);
539
545
  await ensureRevokeReposExist(repoSources);
540
546
  await installDaemon({ host, hostLabel: parsed.hostLabel || host, repoSources });
541
- console.log(`Updated the daemon on ${host}. ${repoSources.length} source(s), unchanged.`);
547
+ console.log(`Updated the daemon on ${describeHost(host)}, and restarted it.`);
548
+ console.log(`Its ${repoSources.length} key source(s) were left as they are, along with the keys and`);
549
+ console.log(`signers it has already accepted. Only the daemon itself changed:`);
550
+ for (let repoURL of repoSources) {
551
+ console.log(` ${repoURL}`);
552
+ }
542
553
  }
543
554
 
544
555
  async function listSources(host: string) {
545
556
  let remoteConfig = await readRemoteConfig(host);
546
557
  if (!remoteConfig.repoSources.length) {
547
- console.log(`${host} has no key sources. root's authorized_keys is left exactly as it is.`);
558
+ console.log(`${describeHost(host)} has no key sources. root's authorized_keys is left exactly as it is.`);
548
559
  return;
549
560
  }
550
- console.log(`${host} lets root log in with the keys from ${remoteConfig.repoSources.length} repo(s):`);
561
+ console.log(`${describeHost(host)} lets root log in with the keys from ${remoteConfig.repoSources.length} repo(s):`);
551
562
  let merged = new Set<string>();
552
563
  for (let repoURL of remoteConfig.repoSources) {
553
564
  let keys = await readRemoteSourceKeys({ host, repoURL });
@@ -578,11 +589,16 @@ function parseArgs(argv: string[]) {
578
589
  throw new Error(`Expected one of ${VERBS.join(", ")}, was ${verbs.join(" and ")}\n${USAGE}`);
579
590
  }
580
591
  let verb = verbs[0];
581
- let [host, ...rest] = argv.filter(arg => arg !== verb);
582
- if (!host) {
583
- throw new Error(`Expected a host, was nothing\n${USAGE}`);
584
- }
585
- return { verb, host, rest };
592
+ let positional = argv.filter(arg => arg !== verb);
593
+ // A host, when there is one, comes first and is a bare name or address. Everything else that
594
+ // can appear here is a path or a repo url, and those all carry a slash, which is what tells
595
+ // them apart. So no host at all means this machine.
596
+ let host = THIS_MACHINE;
597
+ if (positional.length && !/[\/~\\]/.test(positional[0])) {
598
+ host = positional[0];
599
+ positional = positional.slice(1);
600
+ }
601
+ return { verb, host, rest: positional };
586
602
  }
587
603
 
588
604
  export async function main() {
@@ -4,17 +4,20 @@ import path from "path";
4
4
  import { runPromise } from "socket-function/src/runPromise";
5
5
  import { keyFingerprint, normalizeKeys } from "./authorizedKeys";
6
6
  import { revokeRepoURL } from "./revokeSource";
7
+ import { signRepo } from "../signedFiles/signFiles";
7
8
  import { readRepoKeys } from "./authorizedKeys";
8
9
  import { expandHome } from "../helpers/paths";
9
10
  import { spawnPromise } from "../helpers/spawn";
10
11
 
11
12
  const UNREVOKES_DIR = "unrevoked";
12
13
  const REVOCATIONS_DIR = "revocations";
13
- const USAGE = `Usage: yarn unrevoke [keys-repo]
14
+ const GIT_KEYWORD = "git";
15
+ const COMMIT_MESSAGE = "unrevoke keys";
16
+ const USAGE = `Usage: yarn unrevoke [keys-repo] [${GIT_KEYWORD}]
14
17
 
15
18
  Run this in a keys repo, or name one. It reads that repo's revoke repo and writes one unrevoke file
16
19
  naming every revocation in it, so the keys are accepted again once each machine's hour long wait
17
- passes.
20
+ passes. It signs the result, and with ${GIT_KEYWORD} it commits and pushes it too.
18
21
 
19
22
  Keys that were revoked should normally be deleted from the repo instead. Unrevoking only matters
20
23
  for a key you still want.`;
@@ -92,17 +95,24 @@ export async function revokedKeysInRepo(config: { repoPath: string; sourceURL: s
92
95
  repo that actually holds keys and has an origin to clone from. */
93
96
  async function resolveKeysRepo(named: string | undefined) {
94
97
  let cwd = named && expandHome(named) || undefined;
95
- let repoPath = (await runPromise("git rev-parse --show-toplevel", { quiet: true, cwd })).trim();
96
- if (!repoPath) {
97
- throw new Error(`Expected ${named || "the current directory"} to be inside a git repo, it is not.\n${USAGE}`);
98
+ // spawnPromise rather than runPromise, because these two are read for their value. runPromise
99
+ // returns stdout and stderr joined, so one git warning ends up glued to the front of the path.
100
+ let topLevel = await spawnPromise({ command: "git", args: ["rev-parse", "--show-toplevel"], cwd });
101
+ let repoPath = topLevel.stdout.trim();
102
+ if (topLevel.status !== 0 || !repoPath) {
103
+ throw new Error(
104
+ `Expected ${named || "the current directory"} to be inside a git repo, it is not.\n`
105
+ + `${(topLevel.stdout + topLevel.stderr).trim()}\n${USAGE}`
106
+ );
98
107
  }
99
108
  try {
100
109
  await readRepoKeys(repoPath);
101
110
  } catch (e) {
102
111
  throw new Error(`Expected ${repoPath} to be a keys repo, it holds no keys.\n${e}\n${USAGE}`);
103
112
  }
104
- let originURL = (await runPromise("git remote get-url origin", { quiet: true, cwd: repoPath })).trim();
105
- if (!originURL) {
113
+ let origin = await spawnPromise({ command: "git", args: ["remote", "get-url", "origin"], cwd: repoPath });
114
+ let originURL = origin.stdout.trim();
115
+ if (origin.status !== 0 || !originURL) {
106
116
  throw new Error(`Expected ${repoPath} to have an origin remote, it has none.\n${USAGE}`);
107
117
  }
108
118
  return { repoPath, originURL };
@@ -110,10 +120,12 @@ async function resolveKeysRepo(named: string | undefined) {
110
120
 
111
121
  export async function main() {
112
122
  let argv = process.argv.slice(2);
113
- if (argv.length > 1) {
114
- throw new Error(`Expected at most a keys repo, was ${argv.length} argument(s)\n${USAGE}`);
123
+ let pushToGit = argv.includes(GIT_KEYWORD);
124
+ let positional = argv.filter(arg => arg !== GIT_KEYWORD);
125
+ if (positional.length > 1) {
126
+ throw new Error(`Expected at most a keys repo, was ${positional.length} argument(s)\n${USAGE}`);
115
127
  }
116
- let { repoPath, originURL } = await resolveKeysRepo(argv[0]);
128
+ let { repoPath, originURL } = await resolveKeysRepo(positional[0]);
117
129
 
118
130
  let revocations = await readRemoteRevocations({ sourceURL: originURL });
119
131
  if (!revocations.length) {
@@ -144,6 +156,20 @@ export async function main() {
144
156
  for (let revocation of revocations) {
145
157
  console.log(` ${revocation.fingerprint} revoked by ${revocation.revokedBy || "?"} from ${revocation.attempt?.ip || "?"}`);
146
158
  }
147
- console.log(`Each machine waits an hour after seeing it before the keys work again.`);
148
- console.log(`Sign and publish it with:\n yarn signfiles git`);
159
+
160
+ // Signed here rather than left as a step to remember. An unrevoke nobody signed does nothing at
161
+ // all, and the repo would sit there looking done while every machine ignored it.
162
+ await signRepo({ repoPath });
163
+
164
+ if (!pushToGit) {
165
+ console.log(`\nCommit and push it, and each machine will wait an hour after seeing it before`);
166
+ console.log(`those keys work again:`);
167
+ console.log(`\`\`\`\ngit add -A\ngit commit -m "${COMMIT_MESSAGE}"\ngit push\n\`\`\``);
168
+ return;
169
+ }
170
+ await runPromise(`git add -A`, { cwd: repoPath });
171
+ await runPromise(`git commit -m "${COMMIT_MESSAGE}"`, { cwd: repoPath });
172
+ await runPromise(`git push`, { cwd: repoPath });
173
+ console.log(`\nCommitted and pushed. Each machine waits an hour after seeing it before those`);
174
+ console.log(`keys work again.`);
149
175
  }
@@ -8,28 +8,39 @@ const MAX_ERROR_BODY_LENGTH = 500;
8
8
  // /etc is root owned, so fall back to sudo whenever the SSH user is not root.
9
9
  export const SUDO_PREAMBLE = `SUDO=""; if [ "$(id -u)" -ne 0 ]; then SUDO="sudo -n"; fi`;
10
10
 
11
+ /** No host means this machine, so the very same script runs with nothing in front of it. Local and
12
+ remote then do exactly the same thing, rather than being two pieces of code that have to be kept
13
+ saying the same thing. */
14
+ export const THIS_MACHINE = "";
15
+
16
+ export function describeHost(host: string) {
17
+ return host || "this machine";
18
+ }
19
+
11
20
  /** The host string is handed to ssh untouched. Users, keys and ports belong in the caller's ssh
12
21
  config, so BatchMode makes a missing setup fail immediately instead of prompting. */
13
22
  export async function runOverSSH(config: { host: string; script: string; input?: string; allowFailure?: boolean }) {
14
23
  let { host, script, input, allowFailure } = config;
15
24
  let result = await spawnPromise({
16
- command: "ssh",
17
- args: [
25
+ command: host && "ssh" || "sh",
26
+ args: host && [
18
27
  "-o", "BatchMode=yes",
19
28
  "-o", `ConnectTimeout=${SSH_CONNECT_TIMEOUT}`,
20
29
  host,
21
30
  script,
22
- ],
31
+ ] || ["-c", script],
23
32
  input,
24
33
  inheritStderr: !allowFailure,
25
34
  });
26
35
  if (result.error) {
27
- throw new Error(`Expected ssh to run against ${host}, failed with ${result.error.message}`);
36
+ throw new Error(`Expected to run against ${describeHost(host)}, failed with ${result.error.message}`);
28
37
  }
29
38
  if (result.status !== 0 && !allowFailure) {
39
+ let advice = host
40
+ && ` Non-interactive ssh access to ${host} has to work on its own - fix it in your ssh config.`
41
+ || "";
30
42
  throw new Error(
31
- `Expected ssh to ${host} to exit 0, was ${result.status}.`
32
- + ` Non-interactive ssh access to ${host} has to work on its own - fix it in your ssh config.\n`
43
+ `Expected the script on ${describeHost(host)} to exit 0, was ${result.status}.${advice}\n`
33
44
  + `${(result.stdout + result.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
34
45
  );
35
46
  }
@@ -72,8 +72,25 @@ async function readWebhookFile(filePath: string) {
72
72
  return parseWebhookFile({ contents, sourceName: filePath });
73
73
  }
74
74
 
75
+ /** When the message was sent, in the sending machine's own time zone. Discord shows when it
76
+ received something, which is not the same thing when a machine has been offline or a send has
77
+ been retried, and the zone matters when the machines are not all in one place. */
78
+ export function messageTimestamp(now: Date) {
79
+ let date = now.toLocaleDateString("en-CA");
80
+ let time = now.toLocaleTimeString("en-US", {
81
+ hour: "2-digit",
82
+ minute: "2-digit",
83
+ second: "2-digit",
84
+ hour12: true,
85
+ timeZoneName: "short",
86
+ });
87
+ return `${date} ${time}`;
88
+ }
89
+
75
90
  async function postToWebhook(webhookURL: string, message: string) {
76
- let content = message;
91
+ // Stamped once rather than per attempt, so a retry says when the thing happened rather than
92
+ // when we last managed to get it out.
93
+ let content = `\`${messageTimestamp(new Date())}\` ${message}`;
77
94
  if (content.length > DISCORD_MESSAGE_LIMIT) {
78
95
  content = content.slice(0, DISCORD_MESSAGE_LIMIT - 3) + "...";
79
96
  }
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
6
- //exports.stopDiscordNotifications = exports.sendDiscordNotification = exports.configureDiscordNotifications = exports.sendToWebhookURL = exports.formatWebhookFile = exports.redactWebhookURL = exports.parseWebhookFile = exports.DEFAULT_WEBHOOK_FILE_PATH = void 0;
6
+ //exports.stopDiscordNotifications = exports.sendDiscordNotification = exports.configureDiscordNotifications = exports.sendToWebhookURL = exports.messageTimestamp = exports.formatWebhookFile = exports.redactWebhookURL = exports.parseWebhookFile = exports.DEFAULT_WEBHOOK_FILE_PATH = void 0;
7
7
  const promises_1 = __importDefault(require("fs/promises"));
8
8
  // PORTED CODE: security/authorizedKeys/daemon/portsecureDaemon.js embeds a hand port of this file in plain JS, so the
9
9
  // daemon can run with no dependencies. The two are expected to behave identically - if you change
@@ -67,8 +67,25 @@ async function readWebhookFile(filePath) {
67
67
  }
68
68
  return parseWebhookFile({ contents, sourceName: filePath });
69
69
  }
70
+ /** When the message was sent, in the sending machine's own time zone. Discord shows when it
71
+ received something, which is not the same thing when a machine has been offline or a send has
72
+ been retried, and the zone matters when the machines are not all in one place. */
73
+ function messageTimestamp(now) {
74
+ let date = now.toLocaleDateString("en-CA");
75
+ let time = now.toLocaleTimeString("en-US", {
76
+ hour: "2-digit",
77
+ minute: "2-digit",
78
+ second: "2-digit",
79
+ hour12: true,
80
+ timeZoneName: "short",
81
+ });
82
+ return `${date} ${time}`;
83
+ }
84
+ exports.messageTimestamp = messageTimestamp;
70
85
  async function postToWebhook(webhookURL, message) {
71
- let content = message;
86
+ // Stamped once rather than per attempt, so a retry says when the thing happened rather than
87
+ // when we last managed to get it out.
88
+ let content = `\`${messageTimestamp(new Date())}\` ${message}`;
72
89
  if (content.length > DISCORD_MESSAGE_LIMIT) {
73
90
  content = content.slice(0, DISCORD_MESSAGE_LIMIT - 3) + "...";
74
91
  }
@@ -176,5 +193,5 @@ function stopDiscordNotifications() {
176
193
  notificationState = undefined;
177
194
  }
178
195
  exports.stopDiscordNotifications = stopDiscordNotifications;
179
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY29yZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRpc2NvcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBRTdCLHNIQUFzSDtBQUN0SCxrR0FBa0c7QUFDbEcsOENBQThDO0FBRWpDLFFBQUEseUJBQXlCLEdBQUcsaUNBQWlDLENBQUM7QUFDM0UsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUM3QyxNQUFNLHNCQUFzQixHQUFHO0lBQzNCLG1DQUFtQztJQUNuQyxzQ0FBc0M7SUFDdEMsMENBQTBDO0NBQzdDLENBQUM7QUFDRixNQUFNLHFCQUFxQixHQUFHLElBQUksQ0FBQztBQUNuQyxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUNsQyxNQUFNLGlCQUFpQixHQUFHLENBQUMsQ0FBQztBQUM1QixNQUFNLHNCQUFzQixHQUFHLENBQUMsQ0FBQztBQUNqQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7QUFFekMsSUFBSSxpQkFJUyxDQUFDO0FBRWQsc0ZBQXNGO0FBQ3RGLDRDQUE0QztBQUM1QyxJQUFJLFNBQVMsR0FBcUIsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0FBRXBEO3NDQUNzQztBQUN0QyxTQUFnQixnQkFBZ0IsQ0FBQyxNQUFnRDtJQUM3RSxJQUFJLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUN0QyxJQUFJLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4RyxJQUFJLFVBQVUsR0FBRyxL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QUMsS0FBSyxFQUFFLENBQUM7UUFDVCxNQUFNLElBQUksS0FBSyxDQUFDLCtGQUErRixPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxxQkFBcUIsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUM5SixDQUFDO0lBQ0QsTUFBTSxTQUFTLENBQUMsS0FBSyxDQUFDLFVBQVUsRUFBRSxPQUFPLENBQUMsQ0FBQztBQUMvQyxDQUFDO0FBTkQsMERBTUM7QUFFRCxTQUFnQix3QkFBd0I7SUFDcEMsSUFBSSxDQUFDLGlCQUFpQixFQUFFLENBQUM7UUFDckIsT0FBTztJQUNYLENBQUM7SUFDRCxhQUFhLENBQUMsaUJBQWlCLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDNUMsaUJBQWlCLEdBQUcsU0FBUyxDQUFDO0FBQ2xDLENBQUM7QUFORCw0REFNQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnMvcHJvbWlzZXNcIjtcblxuLy8gUE9SVEVEIENPREU6IHNlY3VyaXR5L2F1dGhvcml6ZWRLZXlzL2RhZW1vbi9wb3J0c2VjdXJlRGFlbW9uLmpzIGVtYmVkcyBhIGhhbmQgcG9ydCBvZiB0aGlzIGZpbGUgaW4gcGxhaW4gSlMsIHNvIHRoZVxuLy8gZGFlbW9uIGNhbiBydW4gd2l0aCBubyBkZXBlbmRlbmNpZXMuIFRoZSB0d28gYXJlIGV4cGVjdGVkIHRvIGJlaGF2ZSBpZGVudGljYWxseSAtIGlmIHlvdSBjaGFuZ2Vcbi8vIG9uZSwgbWFrZSB0aGUgbWF0Y2hpbmcgY2hhbmdlIGluIHRoZSBvdGhlci5cblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEggPSBcIi9ldGMvcG9ydHNlY3VyZS9kaXNjb3JkLXdlYmhvb2tcIjtcbmNvbnN0IERFRkFVTFRfQ0hFQ0tfSU5URVJWQUwgPSA1ICogNjAgKiAxMDAwO1xuY29uc3QgVkFMSURfV0VCSE9PS19QUkVGSVhFUyA9IFtcbiAgICBcImh0dHBzOi8vZGlzY29yZC5jb20vYXBpL3dlYmhvb2tzL1wiLFxuICAgIFwiaHR0cHM6Ly9kaXNjb3JkYXBwLmNvbS9hcGkvd2ViaG9va3MvXCIsXG4gICAgXCJodHRwczovL2NhbmFyeS5kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvXCIsXG5dO1xuY29uc3QgRElTQ09SRF9NRVNTQUdFX0xJTUlUID0gMjAwMDtcbmNvbnN0IE1BWF9FUlJPUl9CT0RZX0xFTkdUSCA9IDUwMDtcbmNvbnN0IE1BWF9TRU5EX0FUVEVNUFRTID0gMztcbmNvbnN0IFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUgPSA4O1xuY29uc3QgREVGQVVMVF9SQVRFX0xJTUlUX1dBSVQgPSAyICogMTAwMDtcblxubGV0IG5vdGlmaWNhdGlvblN0YXRlOiB7XG4gICAgZmlsZVBhdGg6IHN0cmluZztcbiAgICB3ZWJob29rVVJMOiBzdHJpbmc7XG4gICAgY2hlY2tUaW1lcjogTm9kZUpTLlRpbWVvdXQ7XG59IHwgdW5kZWZpbmVkO1xuXG4vLyBEaXNjb3JkIHdlYmhvb2tzIGFyZSByYXRlIGxpbWl0ZWQgKHJvdWdobHkgNSByZXF1ZXN0cyBwZXIgMiBzZWNvbmRzKSwgc28gZXZlcnkgc2VuZFxuLy8gZ29lcyB0aHJvdWdoIG9uZSBjaGFpbiBpbnN0ZWFkIG9mIHJhY2luZy5cbmxldCBzZW5kQ2hhaW46IFByb21pc2U8dW5rbm93bj4gPSBQcm9taXNlLnJlc29sdmUoKTtcblxuLyoqIFB1bGxzIHRoZSB3ZWJob29rIFVSTCBvdXQgb2Ygd2ViaG9vayBmaWxlIGNvbnRlbnRzLiBgc291cmNlTmFtZWAgb25seSBhcHBlYXJzIGluIGVycm9ycywgc29cbiAgICBjYWxsZXJzIGNhbiBuYW1lIGEgcmVtb3RlIHBhdGguICovXG5leHBvcnQgZnVuY3Rpb24gcGFyc2VXZWJob29rRmlsZShjb25maWc6IHsgY29udGVudHM6IHN0cmluZzsgc291cmNlTmFtZTogc3RyaW5nIH0pIHtcbiAgICBsZXQgeyBjb250ZW50cywgc291cmNlTmFtZSB9ID0gY29uZmlnO1xuICAgIGxldCBsaW5lcyA9IGNvbnRlbnRzLnNwbGl0KFwiXFxuXCIpLm1hcChsaW5lID0+IGxpbmUudHJpbSgpKS5maWx0ZXIobGluZSA9PiBsaW5lICYmICFsaW5lLnN0YXJ0c1dpdGgoXCIjXCIpKTtcbiAgICBsZXQgd2ViaG9va1VSTCA9IGxpbmVzWzBdO1xuICAgIGlmICghd2ViaG9va1VSTCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGEgRGlzY29yZCB3ZWJob29rIFVSTCBpbiAke3NvdXJjZU5hbWV9LCB0aGUgZmlsZSBoYXMgbm8gdXNhYmxlIGxpbmVzYCk7XG4gICAgfVxuICAgIGlmICghVkFMSURfV0VCSE9PS19QUkVGSVhFUy5zb21lKHByZWZpeCA9PiB3ZWJob29rVVJMLnN0YXJ0c1dpdGgocHJlZml4KSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYEV4cGVjdGVkIGEgRGlzY29yZCB3ZWJob29rIFVSTCBzdGFydGluZyB3aXRoIG9uZSBvZiAke1ZBTElEX1dFQkhPT0tfUFJFRklYRVMuam9pbihcIiwgXCIpfSwgYFxuICAgICAgICAgICAgKyBgd2FzICR7d2ViaG9va1VSTC5zbGljZSgwLCBNQVhfRVJST1JfQk9EWV9MRU5HVEgpfSAoaW4gJHtzb3VyY2VOYW1lfSlgXG4gICAgICAgICk7XG4gICAgfVxuICAgIHJldHVybiB3ZWJob29rVVJMO1xufVxuXG4vKiogS2VlcHMgdGhlIGlkIGFuZCBib3RoIGVuZHMgb2YgdGhlIHRva2VuLCBzbyBhIHJlYWRlciBjYW4gY29uZmlybSB3aGljaCB3ZWJob29rIHJlcGxhY2VkIHRoZWlyc1xuICAgIHdpdGhvdXQgcmVjZWl2aW5nIG9uZSB0aGV5IGNvdWxkIHBvc3QgdG8uICovXG5leHBvcnQgZnVuY3Rpb24gcmVkYWN0V2ViaG9va1VSTCh3ZWJob29rVVJMOiBzdHJpbmcpIHtcbiAgICBsZXQgc2VwYXJhdG9ySW5kZXggPSB3ZWJob29rVVJMLmxhc3RJbmRleE9mKFwiL1wiKTtcbiAgICBsZXQgYmFzZSA9IHdlYmhvb2tVUkwuc2xpY2UoMCwgc2VwYXJhdG9ySW5kZXggKyAxKTtcbiAgICBsZXQgdG9rZW4gPSB3ZWJob29rVVJMLnNsaWNlKHNlcGFyYXRvckluZGV4ICsgMSk7XG4gICAgaWYgKHRva2VuLmxlbmd0aCA8PSBSRURBQ1RFRF9UT0tFTl9WSVNJQkxFICogMikge1xuICAgICAgICAvLyBUb28gc2hvcnQgdG8gc2hvdyBib3RoIGVuZHMgd2l0aG91dCBnaXZpbmcgYXdheSB0aGUgd2hvbGUgdG9rZW4uXG4gICAgICAgIHJldHVybiBgJHtiYXNlfSR7dG9rZW4uc2xpY2UoMCwgUkVEQUNURURfVE9LRU5fVklTSUJMRSl9Li4uYDtcbiAgICB9XG4gICAgcmV0dXJuIGAke2Jhc2V9JHt0b2tlbi5zbGljZSgwLCBSRURBQ1RFRF9UT0tFTl9WSVNJQkxFKX0uLi4ke3Rva2VuLnNsaWNlKC1SRURBQ1RFRF9UT0tFTl9WSVNJQkxFKX1gO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gZm9ybWF0V2ViaG9va0ZpbGUod2ViaG9va1VSTDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIGAjIHBvcnRzZWN1cmUgRGlzY29yZCB3ZWJob29rLiBGaXJzdCBub24tY29tbWVudCBsaW5lIGlzIHVzZWQuXFxuJHt3ZWJob29rVVJMfVxcbmA7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHJlYWRXZWJob29rRmlsZShmaWxlUGF0aDogc3RyaW5nKSB7XG4gICAgbGV0IGNvbnRlbnRzO1xuICAgIHRyeSB7XG4gICAgICAgIGNvbnRlbnRzID0gYXdhaXQgZnMucmVhZEZpbGUoZmlsZVBhdGgsIFwidXRmOFwiKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgYSByZWFkYWJsZSBEaXNjb3JkIHdlYmhvb2sgZmlsZSBhdCAke2ZpbGVQYXRofSwgJHtlfWApO1xuICAgIH1cbiAgICByZXR1cm4gcGFyc2VXZWJob29rRmlsZSh7IGNvbnRlbnRzLCBzb3VyY2VOYW1lOiBmaWxlUGF0aCB9KTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcG9zdFRvV2ViaG9vayh3ZWJob29rVVJMOiBzdHJpbmcsIG1lc3NhZ2U6IHN0cmluZykge1xuICAgIGxldCBjb250ZW50ID0gbWVzc2FnZTtcbiAgICBpZiAoY29udGVudC5sZW5ndGggPiBESVNDT1JEX01FU1NBR0VfTElNSVQpIHtcbiAgICAgICAgY29udGVudCA9IGNvbnRlbnQuc2xpY2UoMCwgRElTQ09SRF9NRVNTQUdFX0xJTUlUIC0gMykgKyBcIi4uLlwiO1xuICAgIH1cbiAgICBmb3IgKGxldCBhdHRlbXB0ID0gMTsgYXR0ZW1wdCA8PSBNQVhfU0VORF9BVFRFTVBUUzsgYXR0ZW1wdCsrKSB7XG4gICAgICAgIGxldCByZXNwb25zZSA9IGF3YWl0IGZldGNoKHdlYmhvb2tVUkwsIHtcbiAgICAgICAgICAgIG1ldGhvZDogXCJQT1NUXCIsXG4gICAgICAgICAgICBoZWFkZXJzOiB7IFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwiIH0sXG4gICAgICAgICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IGNvbnRlbnQgfSksXG4gICAgICAgIH0pO1xuICAgICAgICBpZiAocmVzcG9uc2Uub2spIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBsZXQgYm9keSA9IChhd2FpdCByZXNwb25zZS50ZXh0KCkpLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCk7XG4gICAgICAgIGlmIChyZXNwb25zZS5zdGF0dXMgPT09IDQyOSAmJiBhdHRlbXB0IDwgTUFYX1NFTkRfQVRURU1QVFMpIHtcbiAgICAgICAgICAgIGxldCByZXRyeUFmdGVyID0gTnVtYmVyKHJlc3BvbnNlLmhlYWRlcnMuZ2V0KFwicmV0cnktYWZ0ZXJcIikpO1xuICAgICAgICAgICAgbGV0IHdhaXRUaW1lID0gcmV0cnlBZnRlciAmJiByZXRyeUFmdGVyICogMTAwMCB8fCBERUZBVUxUX1JBVEVfTElNSVRfV0FJVDtcbiAgICAgICAgICAgIGF3YWl0IG5ldyBQcm9taXNlKHJlc29sdmUgPT4gc2V0VGltZW91dChyZXNvbHZlLCB3YWl0VGltZSkpO1xuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgIH1cbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYEV4cGVjdGVkIGEgMnh4IHJlc3BvbnNlIGZyb20gdGhlIERpc2NvcmQgd2ViaG9vaywgd2FzICR7cmVzcG9uc2Uuc3RhdHVzfSAke3Jlc3BvbnNlLnN0YXR1c1RleHR9LCBib2R5ICR7Ym9keX1gXG4gICAgICAgICk7XG4gICAgfVxufVxuXG5mdW5jdGlvbiBxdWV1ZVNlbmQod2ViaG9va1VSTDogc3RyaW5nLCBtZXNzYWdlOiBzdHJpbmcpIHtcbiAgICBsZXQgc2VuZCA9ICgpID0+IHBvc3RUb1dlYmhvb2sod2ViaG9va1VSTCwgbWVzc2FnZSk7XG4gICAgbGV0IHJlc3VsdCA9IHNlbmRDaGFpbi50aGVuKHNlbmQsIHNlbmQpO1xuICAgIHNlbmRDaGFpbiA9IHJlc3VsdC5jYXRjaCgoKSA9PiB1bmRlZmluZWQpO1xuICAgIHJldHVybiByZXN1bHQ7XG59XG5cbi8qKiBTZW5kcyB0byBhbiBleHBsaWNpdCB3ZWJob29rIFVSTCwgZm9yIHRvb2xpbmcgdGhhdCBhY3RzIG9uIGEgd2ViaG9vayBiZWZvcmUgKG9yIGluc3RlYWQgb2YpXG4gICAgY29uZmlndXJlRGlzY29yZE5vdGlmaWNhdGlvbnMgLSBzZXR1cCwgbWlncmF0aW9ucywgY29ubmVjdGl2aXR5IGNoZWNrcy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZW5kVG9XZWJob29rVVJMKGNvbmZpZzogeyB3ZWJob29rVVJMOiBzdHJpbmc7IG1lc3NhZ2U6IHN0cmluZyB9KSB7XG4gICAgcmV0dXJuIHF1ZXVlU2VuZChjb25maWcud2ViaG9va1VSTCwgY29uZmlnLm1lc3NhZ2UpO1xufVxuXG5hc3luYyBmdW5jdGlvbiBjaGVja1dlYmhvb2tGaWxlQ2hhbmdlZCgpIHtcbiAgICBsZXQgc3RhdGUgPSBub3RpZmljYXRpb25TdGF0ZTtcbiAgICBpZiAoIXN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgbGV0IG5ld1dlYmhvb2tVUkw7XG4gICAgdHJ5IHtcbiAgICAgICAgbmV3V2ViaG9va1VSTCA9IGF3YWl0IHJlYWRXZWJob29rRmlsZShzdGF0ZS5maWxlUGF0aCk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiBEaXNjb3JkIHdlYmhvb2sgZmlsZSBpcyBubyBsb25nZXIgcmVhZGFibGUsIHN0aWxsIHVzaW5nIHRoZSBsb2FkZWQgd2ViaG9vay4gJHtlfWApO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChuZXdXZWJob29rVVJMID09PSBzdGF0ZS53ZWJob29rVVJMKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgLy8gV2FybiB0aGUgb2xkIGNoYW5uZWwgZmlyc3QsIHdpdGggdGhlIG5ldyB3ZWJob29rIHJlZGFjdGVkLCBzbyBhIHN0b2xlbiBvbGQgd2ViaG9vayBkb2VzIG5vdFxuICAgIC8vIGhhbmQgdGhlIGF0dGFja2VyIGEgdXNhYmxlIG5ldyBvbmUuXG4gICAgdHJ5IHtcbiAgICAgICAgYXdhaXQgcXVldWVTZW5kKFxuICAgICAgICAgICAgc3RhdGUud2ViaG9va1VSTCxcbiAgICAgICAgICAgIGAqKnBvcnRzZWN1cmUqKjogdGhlIERpc2NvcmQgd2ViaG9vayBpbiBcXGAke3N0YXRlLmZpbGVQYXRofVxcYCBjaGFuZ2VkIHRvYFxuICAgICAgICAgICAgKyBgIFxcYCR7cmVkYWN0V2ViaG9va1VSTChuZXdXZWJob29rVVJMKX1cXGAuYFxuICAgICAgICAgICAgKyBgIE5vdGlmaWNhdGlvbnMgYXJlIG1vdmluZyB0byB0aGUgbmV3IHdlYmhvb2sgYW5kIHRoaXMgY2hhbm5lbCB3aWxsIHN0b3AgcmVjZWl2aW5nIHRoZW0uYFxuICAgICAgICApO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogZmFpbGVkIHRvIHdhcm4gdGhlIG9sZCBEaXNjb3JkIHdlYmhvb2sgYWJvdXQgdGhlIGNoYW5nZS4gJHtlfWApO1xuICAgIH1cbiAgICBzdGF0ZS53ZWJob29rVVJMID0gbmV3V2ViaG9va1VSTDtcbn1cblxuLyoqIE11c3QgYmUgY2FsbGVkIG9uY2Ugb24gc3RhcnR1cCwgYmVmb3JlIGFueSBub3RpZmljYXRpb24gaXMgc2VudC4gQWJvcnRzIHRoZSBwcm9jZXNzIGlmIHRoZVxuICAgIHdlYmhvb2sgZmlsZSBpcyBtaXNzaW5nIG9yIGludmFsaWQsIHRoZW4gcmUtY2hlY2tzIHRoZSBmaWxlIG9uIGFuIGludGVydmFsIGFuZCB3YXJucyB0aGUgb2xkXG4gICAgd2ViaG9vayB3aGVuZXZlciBpdCBjaGFuZ2VzLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zKGNvbmZpZz86IHtcbiAgICBmaWxlUGF0aD86IHN0cmluZztcbiAgICBjaGVja0ludGVydmFsPzogbnVtYmVyO1xufSkge1xuICAgIGlmIChub3RpZmljYXRpb25TdGF0ZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIHRvIGJlIGNhbGxlZCBvbmNlLCB3YXMgY2FsbGVkIGFnYWluIChhbHJlYWR5IHVzaW5nICR7bm90aWZpY2F0aW9uU3RhdGUuZmlsZVBhdGh9KWApO1xuICAgIH1cbiAgICBsZXQgZmlsZVBhdGggPSBjb25maWc/LmZpbGVQYXRoIHx8IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEg7XG4gICAgbGV0IGNoZWNrSW50ZXJ2YWwgPSBjb25maWc/LmNoZWNrSW50ZXJ2YWwgfHwgREVGQVVMVF9DSEVDS19JTlRFUlZBTDtcbiAgICBsZXQgd2ViaG9va1VSTDtcbiAgICB0cnkge1xuICAgICAgICB3ZWJob29rVVJMID0gYXdhaXQgcmVhZFdlYmhvb2tGaWxlKGZpbGVQYXRoKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IoYHBvcnRzZWN1cmU6IHJlZnVzaW5nIHRvIHN0YXJ0IHdpdGhvdXQgYSB2YWxpZCBEaXNjb3JkIHdlYmhvb2sgZmlsZS5cXG4ke2V9YCk7XG4gICAgICAgIHByb2Nlc3MuZXhpdCgxKTtcbiAgICB9XG4gICAgbGV0IGNoZWNrVGltZXIgPSBzZXRJbnRlcnZhbCgoKSA9PiB7XG4gICAgICAgIGNoZWNrV2ViaG9va0ZpbGVDaGFuZ2VkKCkuY2F0Y2goZSA9PiBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiBEaXNjb3JkIHdlYmhvb2sgZmlsZSBjaGVjayBmYWlsZWQuICR7ZX1gKSk7XG4gICAgfSwgY2hlY2tJbnRlcnZhbCk7XG4gICAgLy8gVGhlIGNoZWNrIHNob3VsZCBuZXZlciBiZSB0aGUgcmVhc29uIHRoZSBwcm9jZXNzIHN0YXlzIGFsaXZlLlxuICAgIGNoZWNrVGltZXIudW5yZWYoKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHsgZmlsZVBhdGgsIHdlYmhvb2tVUkwsIGNoZWNrVGltZXIgfTtcbiAgICByZXR1cm4geyBmaWxlUGF0aCwgY2hlY2tJbnRlcnZhbCB9O1xufVxuXG4vLyBETyBOT1QgYWRkIG5ldyBjYWxscyB0byB0aGlzLiBFdmVyeSBtZXNzYWdlIGdvZXMgdG8gYSByZWFsIERpc2NvcmQgc2VydmVyIHNvbWVvbmUgcmVhZHMsIHNvIGFcbi8vIG5vdGlmaWNhdGlvbiBpcyBvbmx5IGV2ZXIgYWRkZWQgd2hlbiB0aGUgdXNlciBleHBsaWNpdGx5IGFza3MgZm9yIHRoYXQgc3BlY2lmaWMgY2FzZS4gU3RhcnR1cCxcbi8vIHN1Y2Nlc3MsIGVycm9ycywgcmV0cmllcyBhbmQgcmVjb3ZlcmllcyBhbGwgYmVsb25nIGluIGEgbG9nIGluc3RlYWQuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2VuZERpc2NvcmROb3RpZmljYXRpb24obWVzc2FnZTogc3RyaW5nKSB7XG4gICAgbGV0IHN0YXRlID0gbm90aWZpY2F0aW9uU3RhdGU7XG4gICAgaWYgKCFzdGF0ZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIHRvIGJlIGNhbGxlZCBiZWZvcmUgc2VuZGluZywgd2FzIGNhbGxlZCB3aXRoIG1lc3NhZ2UgJHttZXNzYWdlLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9YCk7XG4gICAgfVxuICAgIGF3YWl0IHF1ZXVlU2VuZChzdGF0ZS53ZWJob29rVVJMLCBtZXNzYWdlKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BEaXNjb3JkTm90aWZpY2F0aW9ucygpIHtcbiAgICBpZiAoIW5vdGlmaWNhdGlvblN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY2xlYXJJbnRlcnZhbChub3RpZmljYXRpb25TdGF0ZS5jaGVja1RpbWVyKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHVuZGVmaW5lZDtcbn1cbiJdfQ==
180
- /* _JS_SOURCE_HASH = "d5d1b1a9e262cd5b805adb2fa7d074d1a92e9a99dd226550e7f055a9766ebd19"; */
196
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY29yZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRpc2NvcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBRTdCLHNIQUFzSDtBQUN0SCxrR0FBa0c7QUFDbEcsOENBQThDO0FBRWpDLFFBQUEseUJBQXlCLEdBQUcsaUNBQWlDLENBQUM7QUFDM0UsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUM3QyxNQUFNLHNCQUFzQixHQUFHO0lBQzNCLG1DQUFtQztJQUNuQyxzQ0FBc0M7SUFDdEMsMENBQTBDO0NBQzdDLENBQUM7QUFDRixNQUFNLHFCQUFxQixHQUFHLElBQUksQ0FBQztBQUNuQyxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUNsQyxNQUFNLGlCQUFpQixHQUFHLENBQUMsQ0FBQztBQUM1QixNQUFNLHNCQUFzQixHQUFHLENBQUMsQ0FBQztBQUNqQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7QUFFekMsSUFBSSxpQkFJUyxDQUFDO0FBRWQsc0ZBQXNGO0FBQ3RGLDRDQUE0QztBQUM1QyxJQUFJLFNBQVMsR0FBcUIsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0FBRXBEO3NDQUNzQztBQUN0QyxTQUFnQixnQkFBZ0IsQ0FBQyxNQUFnRDtJQUM3RSxJQUFJLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUN0QyxJQUFJLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4RyxJQUFJLFVBQVUsR0FBRyxL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RUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsa0RBQWtELENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztJQUMvRyxDQUFDLEVBQUUsYUFBYSxDQUFDLENBQUM7SUFDbEIsZ0VBQWdFO0lBQ2hFLFVBQVUsQ0FBQyxLQUFLLEVBQUUsQ0FBQztJQUNuQixpQkFBaUIsR0FBRyxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsVUFBVSxFQUFFLENBQUM7SUFDekQsT0FBTyxFQUFFLFFBQVEsRUFBRSxhQUFhLEVBQUUsQ0FBQztBQUN2QyxDQUFDO0FBdkJELHNFQXVCQztBQUVELGdHQUFnRztBQUNoRyxpR0FBaUc7QUFDakcsdUVBQXVFO0FBQ2hFLEtBQUssVUFBVSx1QkFBdUIsQ0FBQyxPQUFlO0lBQ3pELElBQUksS0FBSyxHQUFHLGlCQUFpQixDQUFDO0lBQzlCLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUNULE1BQU0sSUFBSSxLQUFLLENBQUMsK0ZBQStGLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLHFCQUFxQixDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQzlKLENBQUM7SUFDRCxNQUFNLFNBQVMsQ0FBQyxLQUFLLENBQUMsVUFBVSxFQUFFLE9BQU8sQ0FBQyxDQUFDO0FBQy9DLENBQUM7QUFORCwwREFNQztBQUVELFNBQWdCLHdCQUF3QjtJQUNwQyxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztRQUNyQixPQUFPO0lBQ1gsQ0FBQztJQUNELGFBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUM1QyxpQkFBaUIsR0FBRyxTQUFTLENBQUM7QUFDbEMsQ0FBQztBQU5ELDREQU1DIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZzIGZyb20gXCJmcy9wcm9taXNlc1wiO1xuXG4vLyBQT1JURUQgQ09ERTogc2VjdXJpdHkvYXV0aG9yaXplZEtleXMvZGFlbW9uL3BvcnRzZWN1cmVEYWVtb24uanMgZW1iZWRzIGEgaGFuZCBwb3J0IG9mIHRoaXMgZmlsZSBpbiBwbGFpbiBKUywgc28gdGhlXG4vLyBkYWVtb24gY2FuIHJ1biB3aXRoIG5vIGRlcGVuZGVuY2llcy4gVGhlIHR3byBhcmUgZXhwZWN0ZWQgdG8gYmVoYXZlIGlkZW50aWNhbGx5IC0gaWYgeW91IGNoYW5nZVxuLy8gb25lLCBtYWtlIHRoZSBtYXRjaGluZyBjaGFuZ2UgaW4gdGhlIG90aGVyLlxuXG5leHBvcnQgY29uc3QgREVGQVVMVF9XRUJIT09LX0ZJTEVfUEFUSCA9IFwiL2V0Yy9wb3J0c2VjdXJlL2Rpc2NvcmQtd2ViaG9va1wiO1xuY29uc3QgREVGQVVMVF9DSEVDS19JTlRFUlZBTCA9IDUgKiA2MCAqIDEwMDA7XG5jb25zdCBWQUxJRF9XRUJIT09LX1BSRUZJWEVTID0gW1xuICAgIFwiaHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvXCIsXG4gICAgXCJodHRwczovL2Rpc2NvcmRhcHAuY29tL2FwaS93ZWJob29rcy9cIixcbiAgICBcImh0dHBzOi8vY2FuYXJ5LmRpc2NvcmQuY29tL2FwaS93ZWJob29rcy9cIixcbl07XG5jb25zdCBESVNDT1JEX01FU1NBR0VfTElNSVQgPSAyMDAwO1xuY29uc3QgTUFYX0VSUk9SX0JPRFlfTEVOR1RIID0gNTAwO1xuY29uc3QgTUFYX1NFTkRfQVRURU1QVFMgPSAzO1xuY29uc3QgUkVEQUNURURfVE9LRU5fVklTSUJMRSA9IDg7XG5jb25zdCBERUZBVUxUX1JBVEVfTElNSVRfV0FJVCA9IDIgKiAxMDAwO1xuXG5sZXQgbm90aWZpY2F0aW9uU3RhdGU6IHtcbiAgICBmaWxlUGF0aDogc3RyaW5nO1xuICAgIHdlYmhvb2tVUkw6IHN0cmluZztcbiAgICBjaGVja1RpbWVyOiBOb2RlSlMuVGltZW91dDtcbn0gfCB1bmRlZmluZWQ7XG5cbi8vIERpc2NvcmQgd2ViaG9va3MgYXJlIHJhdGUgbGltaXRlZCAocm91Z2hseSA1IHJlcXVlc3RzIHBlciAyIHNlY29uZHMpLCBzbyBldmVyeSBzZW5kXG4vLyBnb2VzIHRocm91Z2ggb25lIGNoYWluIGluc3RlYWQgb2YgcmFjaW5nLlxubGV0IHNlbmRDaGFpbjogUHJvbWlzZTx1bmtub3duPiA9IFByb21pc2UucmVzb2x2ZSgpO1xuXG4vKiogUHVsbHMgdGhlIHdlYmhvb2sgVVJMIG91dCBvZiB3ZWJob29rIGZpbGUgY29udGVudHMuIGBzb3VyY2VOYW1lYCBvbmx5IGFwcGVhcnMgaW4gZXJyb3JzLCBzb1xuICAgIGNhbGxlcnMgY2FuIG5hbWUgYSByZW1vdGUgcGF0aC4gKi9cbmV4cG9ydCBmdW5jdGlvbiBwYXJzZVdlYmhvb2tGaWxlKGNvbmZpZzogeyBjb250ZW50czogc3RyaW5nOyBzb3VyY2VOYW1lOiBzdHJpbmcgfSkge1xuICAgIGxldCB7IGNvbnRlbnRzLCBzb3VyY2VOYW1lIH0gPSBjb25maWc7XG4gICAgbGV0IGxpbmVzID0gY29udGVudHMuc3BsaXQoXCJcXG5cIikubWFwKGxpbmUgPT4gbGluZS50cmltKCkpLmZpbHRlcihsaW5lID0+IGxpbmUgJiYgIWxpbmUuc3RhcnRzV2l0aChcIiNcIikpO1xuICAgIGxldCB3ZWJob29rVVJMID0gbGluZXNbMF07XG4gICAgaWYgKCF3ZWJob29rVVJMKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgYSBEaXNjb3JkIHdlYmhvb2sgVVJMIGluICR7c291cmNlTmFtZX0sIHRoZSBmaWxlIGhhcyBubyB1c2FibGUgbGluZXNgKTtcbiAgICB9XG4gICAgaWYgKCFWQUxJRF9XRUJIT09LX1BSRUZJWEVTLnNvbWUocHJlZml4ID0+IHdlYmhvb2tVUkwuc3RhcnRzV2l0aChwcmVmaXgpKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgICAgICBgRXhwZWN0ZWQgYSBEaXNjb3JkIHdlYmhvb2sgVVJMIHN0YXJ0aW5nIHdpdGggb25lIG9mICR7VkFMSURfV0VCSE9PS19QUkVGSVhFUy5qb2luKFwiLCBcIil9LCBgXG4gICAgICAgICAgICArIGB3YXMgJHt3ZWJob29rVVJMLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9IChpbiAke3NvdXJjZU5hbWV9KWBcbiAgICAgICAgKTtcbiAgICB9XG4gICAgcmV0dXJuIHdlYmhvb2tVUkw7XG59XG5cbi8qKiBLZWVwcyB0aGUgaWQgYW5kIGJvdGggZW5kcyBvZiB0aGUgdG9rZW4sIHNvIGEgcmVhZGVyIGNhbiBjb25maXJtIHdoaWNoIHdlYmhvb2sgcmVwbGFjZWQgdGhlaXJzXG4gICAgd2l0aG91dCByZWNlaXZpbmcgb25lIHRoZXkgY291bGQgcG9zdCB0by4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZWRhY3RXZWJob29rVVJMKHdlYmhvb2tVUkw6IHN0cmluZykge1xuICAgIGxldCBzZXBhcmF0b3JJbmRleCA9IHdlYmhvb2tVUkwubGFzdEluZGV4T2YoXCIvXCIpO1xuICAgIGxldCBiYXNlID0gd2ViaG9va1VSTC5zbGljZSgwLCBzZXBhcmF0b3JJbmRleCArIDEpO1xuICAgIGxldCB0b2tlbiA9IHdlYmhvb2tVUkwuc2xpY2Uoc2VwYXJhdG9ySW5kZXggKyAxKTtcbiAgICBpZiAodG9rZW4ubGVuZ3RoIDw9IFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUgKiAyKSB7XG4gICAgICAgIC8vIFRvbyBzaG9ydCB0byBzaG93IGJvdGggZW5kcyB3aXRob3V0IGdpdmluZyBhd2F5IHRoZSB3aG9sZSB0b2tlbi5cbiAgICAgICAgcmV0dXJuIGAke2Jhc2V9JHt0b2tlbi5zbGljZSgwLCBSRURBQ1RFRF9UT0tFTl9WSVNJQkxFKX0uLi5gO1xuICAgIH1cbiAgICByZXR1cm4gYCR7YmFzZX0ke3Rva2VuLnNsaWNlKDAsIFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUpfS4uLiR7dG9rZW4uc2xpY2UoLVJFREFDVEVEX1RPS0VOX1ZJU0lCTEUpfWA7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBmb3JtYXRXZWJob29rRmlsZSh3ZWJob29rVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYCMgcG9ydHNlY3VyZSBEaXNjb3JkIHdlYmhvb2suIEZpcnN0IG5vbi1jb21tZW50IGxpbmUgaXMgdXNlZC5cXG4ke3dlYmhvb2tVUkx9XFxuYDtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZFdlYmhvb2tGaWxlKGZpbGVQYXRoOiBzdHJpbmcpIHtcbiAgICBsZXQgY29udGVudHM7XG4gICAgdHJ5IHtcbiAgICAgICAgY29udGVudHMgPSBhd2FpdCBmcy5yZWFkRmlsZShmaWxlUGF0aCwgXCJ1dGY4XCIpO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhIHJlYWRhYmxlIERpc2NvcmQgd2ViaG9vayBmaWxlIGF0ICR7ZmlsZVBhdGh9LCAke2V9YCk7XG4gICAgfVxuICAgIHJldHVybiBwYXJzZVdlYmhvb2tGaWxlKHsgY29udGVudHMsIHNvdXJjZU5hbWU6IGZpbGVQYXRoIH0pO1xufVxuXG4vKiogV2hlbiB0aGUgbWVzc2FnZSB3YXMgc2VudCwgaW4gdGhlIHNlbmRpbmcgbWFjaGluZSdzIG93biB0aW1lIHpvbmUuIERpc2NvcmQgc2hvd3Mgd2hlbiBpdFxuICAgIHJlY2VpdmVkIHNvbWV0aGluZywgd2hpY2ggaXMgbm90IHRoZSBzYW1lIHRoaW5nIHdoZW4gYSBtYWNoaW5lIGhhcyBiZWVuIG9mZmxpbmUgb3IgYSBzZW5kIGhhc1xuICAgIGJlZW4gcmV0cmllZCwgYW5kIHRoZSB6b25lIG1hdHRlcnMgd2hlbiB0aGUgbWFjaGluZXMgYXJlIG5vdCBhbGwgaW4gb25lIHBsYWNlLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG1lc3NhZ2VUaW1lc3RhbXAobm93OiBEYXRlKSB7XG4gICAgbGV0IGRhdGUgPSBub3cudG9Mb2NhbGVEYXRlU3RyaW5nKFwiZW4tQ0FcIik7XG4gICAgbGV0IHRpbWUgPSBub3cudG9Mb2NhbGVUaW1lU3RyaW5nKFwiZW4tVVNcIiwge1xuICAgICAgICBob3VyOiBcIjItZGlnaXRcIixcbiAgICAgICAgbWludXRlOiBcIjItZGlnaXRcIixcbiAgICAgICAgc2Vjb25kOiBcIjItZGlnaXRcIixcbiAgICAgICAgaG91cjEyOiB0cnVlLFxuICAgICAgICB0aW1lWm9uZU5hbWU6IFwic2hvcnRcIixcbiAgICB9KTtcbiAgICByZXR1cm4gYCR7ZGF0ZX0gJHt0aW1lfWA7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHBvc3RUb1dlYmhvb2sod2ViaG9va1VSTDogc3RyaW5nLCBtZXNzYWdlOiBzdHJpbmcpIHtcbiAgICAvLyBTdGFtcGVkIG9uY2UgcmF0aGVyIHRoYW4gcGVyIGF0dGVtcHQsIHNvIGEgcmV0cnkgc2F5cyB3aGVuIHRoZSB0aGluZyBoYXBwZW5lZCByYXRoZXIgdGhhblxuICAgIC8vIHdoZW4gd2UgbGFzdCBtYW5hZ2VkIHRvIGdldCBpdCBvdXQuXG4gICAgbGV0IGNvbnRlbnQgPSBgXFxgJHttZXNzYWdlVGltZXN0YW1wKG5ldyBEYXRlKCkpfVxcYCAke21lc3NhZ2V9YDtcbiAgICBpZiAoY29udGVudC5sZW5ndGggPiBESVNDT1JEX01FU1NBR0VfTElNSVQpIHtcbiAgICAgICAgY29udGVudCA9IGNvbnRlbnQuc2xpY2UoMCwgRElTQ09SRF9NRVNTQUdFX0xJTUlUIC0gMykgKyBcIi4uLlwiO1xuICAgIH1cbiAgICBmb3IgKGxldCBhdHRlbXB0ID0gMTsgYXR0ZW1wdCA8PSBNQVhfU0VORF9BVFRFTVBUUzsgYXR0ZW1wdCsrKSB7XG4gICAgICAgIGxldCByZXNwb25zZSA9IGF3YWl0IGZldGNoKHdlYmhvb2tVUkwsIHtcbiAgICAgICAgICAgIG1ldGhvZDogXCJQT1NUXCIsXG4gICAgICAgICAgICBoZWFkZXJzOiB7IFwiQ29udGVudC1UeXBlXCI6IFwiYXBwbGljYXRpb24vanNvblwiIH0sXG4gICAgICAgICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IGNvbnRlbnQgfSksXG4gICAgICAgIH0pO1xuICAgICAgICBpZiAocmVzcG9uc2Uub2spIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBsZXQgYm9keSA9IChhd2FpdCByZXNwb25zZS50ZXh0KCkpLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCk7XG4gICAgICAgIGlmIChyZXNwb25zZS5zdGF0dXMgPT09IDQyOSAmJiBhdHRlbXB0IDwgTUFYX1NFTkRfQVRURU1QVFMpIHtcbiAgICAgICAgICAgIGxldCByZXRyeUFmdGVyID0gTnVtYmVyKHJlc3BvbnNlLmhlYWRlcnMuZ2V0KFwicmV0cnktYWZ0ZXJcIikpO1xuICAgICAgICAgICAgbGV0IHdhaXRUaW1lID0gcmV0cnlBZnRlciAmJiByZXRyeUFmdGVyICogMTAwMCB8fCBERUZBVUxUX1JBVEVfTElNSVRfV0FJVDtcbiAgICAgICAgICAgIGF3YWl0IG5ldyBQcm9taXNlKHJlc29sdmUgPT4gc2V0VGltZW91dChyZXNvbHZlLCB3YWl0VGltZSkpO1xuICAgICAgICAgICAgY29udGludWU7XG4gICAgICAgIH1cbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgYEV4cGVjdGVkIGEgMnh4IHJlc3BvbnNlIGZyb20gdGhlIERpc2NvcmQgd2ViaG9vaywgd2FzICR7cmVzcG9uc2Uuc3RhdHVzfSAke3Jlc3BvbnNlLnN0YXR1c1RleHR9LCBib2R5ICR7Ym9keX1gXG4gICAgICAgICk7XG4gICAgfVxufVxuXG5mdW5jdGlvbiBxdWV1ZVNlbmQod2ViaG9va1VSTDogc3RyaW5nLCBtZXNzYWdlOiBzdHJpbmcpIHtcbiAgICBsZXQgc2VuZCA9ICgpID0+IHBvc3RUb1dlYmhvb2sod2ViaG9va1VSTCwgbWVzc2FnZSk7XG4gICAgbGV0IHJlc3VsdCA9IHNlbmRDaGFpbi50aGVuKHNlbmQsIHNlbmQpO1xuICAgIHNlbmRDaGFpbiA9IHJlc3VsdC5jYXRjaCgoKSA9PiB1bmRlZmluZWQpO1xuICAgIHJldHVybiByZXN1bHQ7XG59XG5cbi8qKiBTZW5kcyB0byBhbiBleHBsaWNpdCB3ZWJob29rIFVSTCwgZm9yIHRvb2xpbmcgdGhhdCBhY3RzIG9uIGEgd2ViaG9vayBiZWZvcmUgKG9yIGluc3RlYWQgb2YpXG4gICAgY29uZmlndXJlRGlzY29yZE5vdGlmaWNhdGlvbnMgLSBzZXR1cCwgbWlncmF0aW9ucywgY29ubmVjdGl2aXR5IGNoZWNrcy4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZW5kVG9XZWJob29rVVJMKGNvbmZpZzogeyB3ZWJob29rVVJMOiBzdHJpbmc7IG1lc3NhZ2U6IHN0cmluZyB9KSB7XG4gICAgcmV0dXJuIHF1ZXVlU2VuZChjb25maWcud2ViaG9va1VSTCwgY29uZmlnLm1lc3NhZ2UpO1xufVxuXG5hc3luYyBmdW5jdGlvbiBjaGVja1dlYmhvb2tGaWxlQ2hhbmdlZCgpIHtcbiAgICBsZXQgc3RhdGUgPSBub3RpZmljYXRpb25TdGF0ZTtcbiAgICBpZiAoIXN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgbGV0IG5ld1dlYmhvb2tVUkw7XG4gICAgdHJ5IHtcbiAgICAgICAgbmV3V2ViaG9va1VSTCA9IGF3YWl0IHJlYWRXZWJob29rRmlsZShzdGF0ZS5maWxlUGF0aCk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiBEaXNjb3JkIHdlYmhvb2sgZmlsZSBpcyBubyBsb25nZXIgcmVhZGFibGUsIHN0aWxsIHVzaW5nIHRoZSBsb2FkZWQgd2ViaG9vay4gJHtlfWApO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChuZXdXZWJob29rVVJMID09PSBzdGF0ZS53ZWJob29rVVJMKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgLy8gV2FybiB0aGUgb2xkIGNoYW5uZWwgZmlyc3QsIHdpdGggdGhlIG5ldyB3ZWJob29rIHJlZGFjdGVkLCBzbyBhIHN0b2xlbiBvbGQgd2ViaG9vayBkb2VzIG5vdFxuICAgIC8vIGhhbmQgdGhlIGF0dGFja2VyIGEgdXNhYmxlIG5ldyBvbmUuXG4gICAgdHJ5IHtcbiAgICAgICAgYXdhaXQgcXVldWVTZW5kKFxuICAgICAgICAgICAgc3RhdGUud2ViaG9va1VSTCxcbiAgICAgICAgICAgIGAqKnBvcnRzZWN1cmUqKjogdGhlIERpc2NvcmQgd2ViaG9vayBpbiBcXGAke3N0YXRlLmZpbGVQYXRofVxcYCBjaGFuZ2VkIHRvYFxuICAgICAgICAgICAgKyBgIFxcYCR7cmVkYWN0V2ViaG9va1VSTChuZXdXZWJob29rVVJMKX1cXGAuYFxuICAgICAgICAgICAgKyBgIE5vdGlmaWNhdGlvbnMgYXJlIG1vdmluZyB0byB0aGUgbmV3IHdlYmhvb2sgYW5kIHRoaXMgY2hhbm5lbCB3aWxsIHN0b3AgcmVjZWl2aW5nIHRoZW0uYFxuICAgICAgICApO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogZmFpbGVkIHRvIHdhcm4gdGhlIG9sZCBEaXNjb3JkIHdlYmhvb2sgYWJvdXQgdGhlIGNoYW5nZS4gJHtlfWApO1xuICAgIH1cbiAgICBzdGF0ZS53ZWJob29rVVJMID0gbmV3V2ViaG9va1VSTDtcbn1cblxuLyoqIE11c3QgYmUgY2FsbGVkIG9uY2Ugb24gc3RhcnR1cCwgYmVmb3JlIGFueSBub3RpZmljYXRpb24gaXMgc2VudC4gQWJvcnRzIHRoZSBwcm9jZXNzIGlmIHRoZVxuICAgIHdlYmhvb2sgZmlsZSBpcyBtaXNzaW5nIG9yIGludmFsaWQsIHRoZW4gcmUtY2hlY2tzIHRoZSBmaWxlIG9uIGFuIGludGVydmFsIGFuZCB3YXJucyB0aGUgb2xkXG4gICAgd2ViaG9vayB3aGVuZXZlciBpdCBjaGFuZ2VzLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zKGNvbmZpZz86IHtcbiAgICBmaWxlUGF0aD86IHN0cmluZztcbiAgICBjaGVja0ludGVydmFsPzogbnVtYmVyO1xufSkge1xuICAgIGlmIChub3RpZmljYXRpb25TdGF0ZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIHRvIGJlIGNhbGxlZCBvbmNlLCB3YXMgY2FsbGVkIGFnYWluIChhbHJlYWR5IHVzaW5nICR7bm90aWZpY2F0aW9uU3RhdGUuZmlsZVBhdGh9KWApO1xuICAgIH1cbiAgICBsZXQgZmlsZVBhdGggPSBjb25maWc/LmZpbGVQYXRoIHx8IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEg7XG4gICAgbGV0IGNoZWNrSW50ZXJ2YWwgPSBjb25maWc/LmNoZWNrSW50ZXJ2YWwgfHwgREVGQVVMVF9DSEVDS19JTlRFUlZBTDtcbiAgICBsZXQgd2ViaG9va1VSTDtcbiAgICB0cnkge1xuICAgICAgICB3ZWJob29rVVJMID0gYXdhaXQgcmVhZFdlYmhvb2tGaWxlKGZpbGVQYXRoKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IoYHBvcnRzZWN1cmU6IHJlZnVzaW5nIHRvIHN0YXJ0IHdpdGhvdXQgYSB2YWxpZCBEaXNjb3JkIHdlYmhvb2sgZmlsZS5cXG4ke2V9YCk7XG4gICAgICAgIHByb2Nlc3MuZXhpdCgxKTtcbiAgICB9XG4gICAgbGV0IGNoZWNrVGltZXIgPSBzZXRJbnRlcnZhbCgoKSA9PiB7XG4gICAgICAgIGNoZWNrV2ViaG9va0ZpbGVDaGFuZ2VkKCkuY2F0Y2goZSA9PiBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiBEaXNjb3JkIHdlYmhvb2sgZmlsZSBjaGVjayBmYWlsZWQuICR7ZX1gKSk7XG4gICAgfSwgY2hlY2tJbnRlcnZhbCk7XG4gICAgLy8gVGhlIGNoZWNrIHNob3VsZCBuZXZlciBiZSB0aGUgcmVhc29uIHRoZSBwcm9jZXNzIHN0YXlzIGFsaXZlLlxuICAgIGNoZWNrVGltZXIudW5yZWYoKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHsgZmlsZVBhdGgsIHdlYmhvb2tVUkwsIGNoZWNrVGltZXIgfTtcbiAgICByZXR1cm4geyBmaWxlUGF0aCwgY2hlY2tJbnRlcnZhbCB9O1xufVxuXG4vLyBETyBOT1QgYWRkIG5ldyBjYWxscyB0byB0aGlzLiBFdmVyeSBtZXNzYWdlIGdvZXMgdG8gYSByZWFsIERpc2NvcmQgc2VydmVyIHNvbWVvbmUgcmVhZHMsIHNvIGFcbi8vIG5vdGlmaWNhdGlvbiBpcyBvbmx5IGV2ZXIgYWRkZWQgd2hlbiB0aGUgdXNlciBleHBsaWNpdGx5IGFza3MgZm9yIHRoYXQgc3BlY2lmaWMgY2FzZS4gU3RhcnR1cCxcbi8vIHN1Y2Nlc3MsIGVycm9ycywgcmV0cmllcyBhbmQgcmVjb3ZlcmllcyBhbGwgYmVsb25nIGluIGEgbG9nIGluc3RlYWQuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2VuZERpc2NvcmROb3RpZmljYXRpb24obWVzc2FnZTogc3RyaW5nKSB7XG4gICAgbGV0IHN0YXRlID0gbm90aWZpY2F0aW9uU3RhdGU7XG4gICAgaWYgKCFzdGF0ZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIHRvIGJlIGNhbGxlZCBiZWZvcmUgc2VuZGluZywgd2FzIGNhbGxlZCB3aXRoIG1lc3NhZ2UgJHttZXNzYWdlLnNsaWNlKDAsIE1BWF9FUlJPUl9CT0RZX0xFTkdUSCl9YCk7XG4gICAgfVxuICAgIGF3YWl0IHF1ZXVlU2VuZChzdGF0ZS53ZWJob29rVVJMLCBtZXNzYWdlKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BEaXNjb3JkTm90aWZpY2F0aW9ucygpIHtcbiAgICBpZiAoIW5vdGlmaWNhdGlvblN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY2xlYXJJbnRlcnZhbChub3RpZmljYXRpb25TdGF0ZS5jaGVja1RpbWVyKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHVuZGVmaW5lZDtcbn1cbiJdfQ==
197
+ /* _JS_SOURCE_HASH = "be450922cec9e8a268e2a78f817bfedb88063a0b59db5b1a954d81c96169543a"; */
@@ -1,16 +1,25 @@
1
1
  import { parseWebhookFile } from "./discord";
2
2
  import { ensureRemoteWebhook, REPLACE_KEYWORD } from "./remoteWebhook";
3
+ import { describeHost, THIS_MACHINE } from "../helpers/remoteSSH";
3
4
 
4
- const USAGE = `Usage: yarn setupnotify <host> <discord-webhook-url> [${REPLACE_KEYWORD}]`;
5
+ const USAGE = `Usage: yarn setupnotify [host] <discord-webhook-url> [${REPLACE_KEYWORD}]
6
+
7
+ With no host it sets the webhook up on this machine.`;
5
8
 
6
9
  function parseArgs(argv: string[]) {
7
10
  let replace = argv.includes(REPLACE_KEYWORD);
8
11
  let positional = argv.filter(arg => arg !== REPLACE_KEYWORD);
9
- if (positional.length !== 2) {
10
- throw new Error(`Expected a host and a webhook URL, was ${positional.length} argument(s): ${positional.join(" ") || "(none)"}\n${USAGE}`);
12
+ if (!positional.length || positional.length > 2) {
13
+ throw new Error(
14
+ `Expected a webhook URL, and optionally a host before it, was ${positional.length}`
15
+ + ` argument(s): ${positional.join(" ") || "(none)"}\n${USAGE}`
16
+ );
11
17
  }
12
- let [host, webhookURL] = positional;
13
- // Validates the URL shape up front, so we never ssh anywhere with a bad webhook.
18
+ // The webhook url is the last one, so whatever comes before it is the host. Without one this
19
+ // machine is the target.
20
+ let webhookURL = positional[positional.length - 1];
21
+ let host = positional.length > 1 && positional[0] || THIS_MACHINE;
22
+ // Validates the URL shape up front, so we never go anywhere with a bad webhook.
14
23
  parseWebhookFile({ contents: webhookURL, sourceName: "the command line" });
15
24
  return { host, webhookURL, replace };
16
25
  }
@@ -19,11 +28,11 @@ export async function main() {
19
28
  let { host, webhookURL, replace } = parseArgs(process.argv.slice(2));
20
29
  let { outcome, filePath } = await ensureRemoteWebhook({ host, webhookURL, replace });
21
30
  if (outcome === "unchanged") {
22
- console.log(`${host} already has this exact webhook in ${filePath}, nothing to do.`);
31
+ console.log(`${describeHost(host)} already has this exact webhook in ${filePath}, nothing to do.`);
23
32
  return;
24
33
  }
25
34
  if (outcome === "replaced") {
26
- console.log(`Replaced the webhook on ${host} and notified the old one.`);
35
+ console.log(`Replaced the webhook on ${describeHost(host)} and notified the old one.`);
27
36
  }
28
- console.log(`Wrote ${filePath} on ${host} (mode 600), and confirmed on the new webhook.`);
37
+ console.log(`Wrote ${filePath} on ${describeHost(host)} (mode 600), and confirmed on the new webhook.`);
29
38
  }