sliftutils 1.7.129 → 1.7.131

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 (78) hide show
  1. package/index.d.ts +4 -51
  2. package/misc/dist/ownIPs.ts.cache +29 -3
  3. package/misc/dist/strings.ts.cache +2 -2
  4. package/misc/https/dist/certs.ts.cache +2 -2
  5. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  6. package/misc/ownIPs.ts +29 -1
  7. package/package.json +1 -1
  8. package/security/authorizedKeys/authorizedKeys.d.ts +37 -0
  9. package/security/authorizedKeys/daemon/changes.d.ts +17 -0
  10. package/security/authorizedKeys/daemon/dist/changes.ts.cache +2 -2
  11. package/security/authorizedKeys/daemon/dist/git.ts.cache +2 -2
  12. package/security/authorizedKeys/daemon/dist/notify.ts.cache +2 -2
  13. package/security/authorizedKeys/daemon/dist/paths.ts.cache +4 -7
  14. package/security/authorizedKeys/daemon/dist/repoFiles.ts.cache +2 -2
  15. package/security/authorizedKeys/daemon/dist/revocation.ts.cache +0 -0
  16. package/security/authorizedKeys/daemon/dist/sessions.ts.cache +2 -2
  17. package/security/authorizedKeys/daemon/dist/state.ts.cache +6 -4
  18. package/security/authorizedKeys/daemon/dist/trust.ts.cache +2 -2
  19. package/security/authorizedKeys/daemon/git.d.ts +35 -0
  20. package/security/authorizedKeys/daemon/notify.d.ts +8 -0
  21. package/security/authorizedKeys/daemon/paths.d.ts +15 -0
  22. package/security/authorizedKeys/daemon/paths.ts +0 -3
  23. package/security/authorizedKeys/daemon/repoFiles.d.ts +25 -0
  24. package/security/authorizedKeys/daemon/revocation.d.ts +74 -0
  25. package/security/authorizedKeys/daemon/revocation.ts +0 -0
  26. package/security/authorizedKeys/daemon/sessions.d.ts +19 -0
  27. package/security/authorizedKeys/daemon/state.d.ts +67 -0
  28. package/security/authorizedKeys/daemon/state.ts +4 -8
  29. package/security/authorizedKeys/daemon/trust.d.ts +20 -0
  30. package/security/authorizedKeys/dist/authorizedKeys.ts.cache +2 -2
  31. package/security/authorizedKeys/dist/keysRepo.ts.cache +111 -0
  32. package/security/authorizedKeys/dist/revokeSource.ts.cache +2 -2
  33. package/security/authorizedKeys/dist/secureSSH.ts.cache +147 -71
  34. package/security/authorizedKeys/dist/sources.ts.cache +2 -2
  35. package/security/authorizedKeys/dist/unrevoke.ts.cache +70 -51
  36. package/security/authorizedKeys/keysRepo.d.ts +10 -0
  37. package/security/authorizedKeys/keysRepo.ts +111 -0
  38. package/security/authorizedKeys/revokeSource.d.ts +13 -0
  39. package/security/authorizedKeys/secureSSH.ts +28 -38
  40. package/security/authorizedKeys/sources.d.ts +17 -0
  41. package/security/authorizedKeys/unrevoke.ts +9 -36
  42. package/security/helpers/dist/paths.ts.cache +2 -2
  43. package/security/helpers/dist/spawn.ts.cache +2 -2
  44. package/security/helpers/paths.d.ts +4 -0
  45. package/security/helpers/spawn.d.ts +16 -0
  46. package/security/keys/deriveKey.d.ts +16 -0
  47. package/security/keys/dist/deriveKey.ts.cache +2 -2
  48. package/security/keys/dist/sshKeyFile.ts.cache +2 -2
  49. package/security/keys/sshKeyFile.d.ts +18 -0
  50. package/security/machines/addMachine.ts +80 -27
  51. package/security/machines/dist/addMachine.ts.cache +6 -5
  52. package/security/machines/dist/machines.ts.cache +110 -101
  53. package/security/machines/machines.d.ts +81 -0
  54. package/security/machines/machines.ts +123 -99
  55. package/security/machines/trustServer.ts +5 -3
  56. package/security/notifications/discord.d.ts +36 -0
  57. package/security/notifications/dist/discord.ts.cache +2 -2
  58. package/security/notifications/dist/remoteWebhook.ts.cache +78 -0
  59. package/security/notifications/dist/setupNotify.ts.cache +39 -0
  60. package/security/signedFiles/dist/manifest.ts.cache +2 -2
  61. package/security/signedFiles/dist/signFiles.ts.cache +8 -10
  62. package/security/signedFiles/manifest.d.ts +44 -0
  63. package/security/signedFiles/signFiles.ts +6 -10
  64. package/storage/remoteStorage/ArchivesRemote.ts +8 -10
  65. package/storage/remoteStorage/accessPage.tsx +13 -90
  66. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +11 -12
  67. package/storage/remoteStorage/dist/serverConfig.ts.cache +7 -49
  68. package/storage/remoteStorage/dist/storageController.ts.cache +33 -114
  69. package/storage/remoteStorage/serverConfig.d.ts +0 -10
  70. package/storage/remoteStorage/serverConfig.ts +6 -48
  71. package/storage/remoteStorage/storageController.d.ts +4 -36
  72. package/storage/remoteStorage/storageController.ts +40 -126
  73. package/storage/remoteStorage/storageServer.ts +1 -8
  74. package/storage/remoteStorage/storageServerCli.ts +14 -0
  75. package/security/machines/trustState.ts +0 -53
  76. package/storage/remoteStorage/grantAccess.js +0 -4
  77. package/storage/remoteStorage/grantAccessCli.d.ts +0 -1
  78. package/storage/remoteStorage/grantAccessCli.ts +0 -25
@@ -0,0 +1,81 @@
1
+ export declare const MACHINES_DIR = "machines";
2
+ /** One machine we are willing to talk to, and the addresses it may talk to us from. */
3
+ export type MachineState = {
4
+ machineId: string;
5
+ ips: string[];
6
+ addedAt: string;
7
+ };
8
+ /** This machine's keys repo, worked out once and then remembered.
9
+
10
+ A failure is remembered too, but only briefly: a server may well start before the machine has
11
+ been set up, and should start working when it is, rather than needing a restart. */
12
+ export declare const keysRepo: {
13
+ (): Promise<{
14
+ repoPath: string;
15
+ sourceURL: string;
16
+ }>;
17
+ reset(): void;
18
+ set(newValue: Promise<{
19
+ repoPath: string;
20
+ sourceURL: string;
21
+ }>): void;
22
+ };
23
+ /** Every machine a checkout lists. Without a repo, this machine's own. */
24
+ export declare function getMachines(repoPath?: string): Promise<MachineState[]>;
25
+ /** The machines this system trusts, as the signed repo lists them.
26
+
27
+ Held rather than read per call, and refreshed in the background, so nobody asking whether a
28
+ machine is trusted waits for a directory of files to be read and a signature to be checked. A
29
+ refresh that fails leaves the list we already have: the checkout is only stale, not wrong. */
30
+ export declare function getTrustedMachines(): Promise<MachineState[]>;
31
+ /** Makes a checkout list exactly these machines: those named are written with the addresses given,
32
+ and anything not named is removed. So read them, change what you want, and write the result.
33
+
34
+ The addresses are part of setting rather than a separate step, because a machine with no
35
+ address it may talk from is not a machine that would ever be accepted - there is no state where
36
+ naming one without them means anything.
37
+
38
+ Nothing here signs or commits. The signature is what every other machine checks before
39
+ believing any of this, and it takes the hardware key, so `yarn signfiles git` is still yours to
40
+ run afterwards. */
41
+ export declare function setMachines(config: {
42
+ repoPath?: string;
43
+ machines: {
44
+ machineId: string;
45
+ ips: string[];
46
+ }[];
47
+ }): Promise<MachineState[]>;
48
+ /** What to run to trust a machine that has just been refused.
49
+
50
+ Everything in it is already known to whoever is being refused: their machine id, the address we
51
+ saw them at, and the domain they just talked to. Handing it back saves them working out the
52
+ parts of a command they have every right to know. Running it still takes the hardware key on
53
+ the machine that owns the repo, so telling them costs nothing. */
54
+ export declare function addMachineCommand(config: {
55
+ machineId: string;
56
+ ip: string;
57
+ domain?: string;
58
+ }): string;
59
+ /** Whether we will talk to this machine, coming from this address.
60
+
61
+ Both halves are required and neither is a guess: the caller knows the machine id because the
62
+ connection proved it, and knows the address because the packets came from there. A machine we
63
+ do not list is simply not accepted. A machine we do list, arriving from an address it does not
64
+ have, is treated as the same kind of event as a stolen ssh key - it is revoked everywhere, and
65
+ stays revoked until an unrevoke allows that machine from that address.
66
+
67
+ Says why when the answer is no, because the three ways to be refused are entirely different
68
+ things to a person reading it: never trusted at all, trusted but frozen, or trusted and talking
69
+ from somewhere it should not be.
70
+
71
+ Throws when the repo itself cannot be read or is not signed, rather than answering false: that
72
+ is a broken installation, not a rejected machine, and the two deserve different handling. */
73
+ export type MachineVerdict = {
74
+ accepted: boolean;
75
+ reason: string;
76
+ };
77
+ export declare function isMachineAccepted(config: {
78
+ machineId: string;
79
+ ip: string;
80
+ domain?: string;
81
+ }): Promise<MachineVerdict>;
@@ -1,7 +1,7 @@
1
1
  import fs from "fs/promises";
2
2
  import os from "os";
3
3
  import path from "path";
4
- import { CONFIG_PATH } from "../authorizedKeys/daemon/paths";
4
+ import { resolveKeysRepo } from "../authorizedKeys/keysRepo";
5
5
  import { revokeRepo, syncRepoFiles } from "../authorizedKeys/daemon/repoFiles";
6
6
  import { listRepoDir, readRepoFile } from "../authorizedKeys/daemon/repoFiles";
7
7
  import { newRevocationId, pairKey, readRevocationFiles, readUnrevokes } from "../authorizedKeys/daemon/revocation";
@@ -9,10 +9,10 @@ import { runGit } from "../authorizedKeys/daemon/git";
9
9
  import { ensureRevokeKey } from "../authorizedKeys/daemon/repoFiles";
10
10
  import { verifyCheckout } from "../authorizedKeys/daemon/trust";
11
11
  import { revokeRepoPath, revokeRepoURL } from "../authorizedKeys/revokeSource";
12
- import { sourceRepoPath } from "../authorizedKeys/sources";
13
12
  import { notify } from "../authorizedKeys/daemon/notify";
14
13
  import { areDiscordNotificationsConfigured, configureDiscordNotifications, DEFAULT_WEBHOOK_FILE_PATH } from "../notifications/discord";
15
- import { unrevokeInEffect } from "./trustState";
14
+ import { lazy } from "socket-function/src/caching";
15
+ import { runInfinitePoll } from "socket-function/src/batching";
16
16
  import { spawnPromise } from "../helpers/spawn";
17
17
 
18
18
  // Which machines this system talks to, kept in the same repo as the ssh keys. That repo is already
@@ -24,9 +24,14 @@ const REVOCATION_REASON = "a machine talked to us from an unapproved IP";
24
24
  // The revoke repo is pulled at most this often. A check happens per request, and a request must
25
25
  // not cost a round trip to github.
26
26
  const REVOKE_SYNC_INTERVAL = 60 * 1000;
27
- // Where a Windows machine is expected to keep the repo, relative to the working directory. There
28
- // is no daemon there to ask, and no /etc to look in.
29
- const WINDOWS_REPO_PATH = "../authorized_keys";
27
+ // And the machine list is re-read from the checkout on this interval, in the background. The
28
+ // checkout itself only moves when the daemon pulls it, which is on the same sort of interval, so
29
+ // reading more often than this would only re-read the same bytes.
30
+ const MACHINES_REFRESH_INTERVAL = 60 * 1000;
31
+ // How long a failure to find the keys repo is remembered. A long lived process may start before
32
+ // the machine is set up, and should pick it up once it is, without being restarted - but it must
33
+ // not pay for resolving the repo on every request either.
34
+ const REPO_RETRY_DELAY = 15 * 1000;
30
35
 
31
36
  /** One machine we are willing to talk to, and the addresses it may talk to us from. */
32
37
  export type MachineState = {
@@ -48,27 +53,78 @@ function machineFilePath(repoPath: string, machineId: string) {
48
53
  return path.join(repoPath, MACHINES_DIR, `${machineId}.json`);
49
54
  }
50
55
 
51
- /** Reads every machine a checkout lists, or sets them.
56
+ /** This machine's keys repo, worked out once and then remembered.
52
57
 
53
- Passing `machines` makes the repo match it exactly: machines named are written with the
54
- addresses given, and machines not named are removed. That is why the addresses are part of
55
- setting rather than a separate step - a machine with no address it may talk from is not a
56
- machine we would accept anyway, so there is no state where naming one without them means
57
- anything. Read first, change what you want, write the result.
58
+ A failure is remembered too, but only briefly: a server may well start before the machine has
59
+ been set up, and should start working when it is, rather than needing a restart. */
60
+ export const keysRepo = lazy(async () => {
61
+ let lookup = resolveKeysRepo();
62
+ lookup.catch(() => {
63
+ setTimeout(() => keysRepo.reset(), REPO_RETRY_DELAY).unref();
64
+ });
65
+ return await lookup;
66
+ });
67
+
68
+ /** Every machine a checkout lists. Without a repo, this machine's own. */
69
+ export async function getMachines(repoPath?: string): Promise<MachineState[]> {
70
+ return [...(await readMachines(repoPath || (await keysRepo()).repoPath)).values()];
71
+ }
72
+
73
+ /** Reads the machine list, once its signature has been checked. An unsigned machine list, or one
74
+ signed over different contents, is not evidence of anything, so this throws rather than
75
+ quietly trusting nobody. */
76
+ async function readTrustedMachines() {
77
+ let { repoPath } = await keysRepo();
78
+ await verifyCheckout(repoPath);
79
+ return await getMachines(repoPath);
80
+ }
58
81
 
59
- Nothing here signs or commits anything. The signature is what every other machine checks before
82
+ const trustedMachines = lazy(async () => {
83
+ let read = readTrustedMachines();
84
+ read.catch(() => trustedMachines.reset());
85
+ return await read;
86
+ });
87
+
88
+ let refreshing = false;
89
+
90
+ /** The machines this system trusts, as the signed repo lists them.
91
+
92
+ Held rather than read per call, and refreshed in the background, so nobody asking whether a
93
+ machine is trusted waits for a directory of files to be read and a signature to be checked. A
94
+ refresh that fails leaves the list we already have: the checkout is only stale, not wrong. */
95
+ export async function getTrustedMachines(): Promise<MachineState[]> {
96
+ if (!refreshing) {
97
+ refreshing = true;
98
+ runInfinitePoll(MACHINES_REFRESH_INTERVAL, async () => {
99
+ try {
100
+ trustedMachines.set(Promise.resolve(await readTrustedMachines()));
101
+ } catch (e) {
102
+ console.log(`Could not re-read the trusted machines, keeping the ones we have. ${e}`);
103
+ }
104
+ });
105
+ }
106
+ return await trustedMachines();
107
+ }
108
+
109
+ /** Makes a checkout list exactly these machines: those named are written with the addresses given,
110
+ and anything not named is removed. So read them, change what you want, and write the result.
111
+
112
+ The addresses are part of setting rather than a separate step, because a machine with no
113
+ address it may talk from is not a machine that would ever be accepted - there is no state where
114
+ naming one without them means anything.
115
+
116
+ Nothing here signs or commits. The signature is what every other machine checks before
60
117
  believing any of this, and it takes the hardware key, so `yarn signfiles git` is still yours to
61
118
  run afterwards. */
62
- export async function machineState(config: {
63
- repoPath: string;
64
- machines?: { machineId: string; ips: string[] }[];
119
+ export async function setMachines(config: {
120
+ repoPath?: string;
121
+ machines: { machineId: string; ips: string[] }[];
65
122
  }): Promise<MachineState[]> {
66
- let { repoPath, machines } = config;
123
+ let { machines } = config;
124
+ let repoPath = config.repoPath || (await keysRepo()).repoPath;
125
+ // Whatever was held is now stale, whoever reads it next.
126
+ trustedMachines.reset();
67
127
  let existing = await readMachines(repoPath);
68
- if (!machines) {
69
- return [...existing.values()];
70
- }
71
-
72
128
  let directory = path.join(repoPath, MACHINES_DIR);
73
129
  let written: MachineState[] = [];
74
130
  for (let machine of machines) {
@@ -122,61 +178,6 @@ async function readMachines(repoPath: string) {
122
178
  return machines;
123
179
  }
124
180
 
125
- async function isKeysRepo(repoPath: string) {
126
- return await pathExists(path.join(repoPath, ".git"))
127
- && await pathExists(path.join(repoPath, "authorized_keys"));
128
- }
129
-
130
- async function originOf(repoPath: string) {
131
- let origin = await spawnPromise({ command: "git", args: ["remote", "get-url", "origin"], cwd: repoPath });
132
- let url = origin.stdout.trim();
133
- if (origin.status !== 0 || !url) {
134
- throw new Error(`Expected ${repoPath} to have an origin remote, it has none`);
135
- }
136
- return url;
137
- }
138
-
139
- /** The keys repo this machine answers from, and the one anything editing the machine list edits.
140
-
141
- On a host, that is the checkout the daemon already keeps up to date. On Windows there is no
142
- daemon and no /etc, so the repo is expected beside the working directory, which is where a
143
- developer working on both would have it.
144
-
145
- Never the directory the command happens to be run from. Which repo this machine trusts is a
146
- property of the machine, not of where somebody was standing when they typed something. */
147
- export async function resolveKeysRepo() {
148
- if (os.platform() === "win32") {
149
- let repoPath = path.resolve(WINDOWS_REPO_PATH);
150
- if (!await isKeysRepo(repoPath)) {
151
- throw new Error(
152
- `Expected ${repoPath} to be an authorized_keys repo, it is not.\n`
153
- + `On Windows the repo is read from there, so clone it beside this one:\n`
154
- + ` git clone <your authorized_keys repo> ${repoPath}`
155
- );
156
- }
157
- return { repoPath, sourceURL: await originOf(repoPath) };
158
- }
159
-
160
- let config = await fs.readFile(CONFIG_PATH, "utf8").catch(() => "");
161
- let sourceURL = config && (JSON.parse(config).repoSources || [])[0] || "";
162
- if (!sourceURL) {
163
- throw new Error(
164
- `Expected this machine to be set up with an authorized_keys repo, ${CONFIG_PATH} names none.\n`
165
- + `Set it up first:\n`
166
- + ` yarn setupnotify <discord-webhook-url>\n`
167
- + ` yarn securessh add <repo-private-key> <repo-url>`
168
- );
169
- }
170
- let repoPath = sourceRepoPath(sourceURL);
171
- if (!await isKeysRepo(repoPath)) {
172
- throw new Error(
173
- `Expected a checkout of ${sourceURL} at ${repoPath}, there is none.\n`
174
- + `Run \`yarn securessh update\` to put it back.`
175
- );
176
- }
177
- return { repoPath, sourceURL };
178
- }
179
-
180
181
  let lastRevokeSync = 0;
181
182
 
182
183
  /** Pulled at most once a REVOKE_SYNC_INTERVAL, because this is asked per request. A sync that
@@ -218,6 +219,17 @@ async function readMachineRevocations(sourceURL: string) {
218
219
  return revocations;
219
220
  }
220
221
 
222
+ /** What to run to trust a machine that has just been refused.
223
+
224
+ Everything in it is already known to whoever is being refused: their machine id, the address we
225
+ saw them at, and the domain they just talked to. Handing it back saves them working out the
226
+ parts of a command they have every right to know. Running it still takes the hardware key on
227
+ the machine that owns the repo, so telling them costs nothing. */
228
+ export function addMachineCommand(config: { machineId: string; ip: string; domain?: string }) {
229
+ let { machineId, ip, domain } = config;
230
+ return `To trust it, run: yarn addmachine ${domain || "<domain>"} ${machineId} ${ip} git`;
231
+ }
232
+
221
233
  /** Sends the one notification this file is allowed to send, when it can.
222
234
 
223
235
  The daemon configures notifications on startup and would simply send. This can also run in some
@@ -285,7 +297,7 @@ async function recordMachineRevocation(config: {
285
297
  + `\n\nIt is frozen everywhere now, and nothing accepts it.`
286
298
  + `\n\nIf this was an attack, remove \`machines/${machineId}.json\` from \`${sourceURL}\` now.`
287
299
  + `\nIf it was legitimate, run \`yarn unrevoke git\` in that repo. It allows ${ip} for that`
288
- + ` machine, and takes an hour to reach every machine.`
300
+ + ` machine, and takes effect as each machine picks it up.`
289
301
  + `\n\nmachine: \`${machineId}\``
290
302
  + `\nfrozen by: \`${hostLabel}\``
291
303
  );
@@ -299,48 +311,59 @@ async function recordMachineRevocation(config: {
299
311
  have, is treated as the same kind of event as a stolen ssh key - it is revoked everywhere, and
300
312
  stays revoked until an unrevoke allows that machine from that address.
301
313
 
314
+ Says why when the answer is no, because the three ways to be refused are entirely different
315
+ things to a person reading it: never trusted at all, trusted but frozen, or trusted and talking
316
+ from somewhere it should not be.
317
+
302
318
  Throws when the repo itself cannot be read or is not signed, rather than answering false: that
303
319
  is a broken installation, not a rejected machine, and the two deserve different handling. */
304
- export async function isMachineAccepted(config: { machineId: string; ip: string }): Promise<boolean> {
305
- let { machineId, ip } = config;
320
+ export type MachineVerdict = { accepted: boolean; reason: string };
321
+
322
+ export async function isMachineAccepted(config: {
323
+ machineId: string;
324
+ ip: string;
325
+ // Only used to write a complete command into the refusal. A caller that knows which domain it
326
+ // is answering as saves whoever is refused from having to guess it.
327
+ domain?: string;
328
+ }): Promise<MachineVerdict> {
329
+ let { machineId, ip, domain } = config;
306
330
  if (!machineId || !ip) {
307
331
  throw new Error(`Expected a machineId and an ip, was ${JSON.stringify(machineId)} and ${JSON.stringify(ip)}`);
308
332
  }
309
- let { repoPath, sourceURL } = await resolveKeysRepo();
310
- // Unsigned, or signed over different contents, means the machine list is not evidence of
311
- // anything. Same gate the ssh keys go through.
312
- await verifyCheckout(repoPath);
313
-
314
- let machine = (await readMachines(repoPath)).get(machineId);
333
+ let { sourceURL } = await keysRepo();
334
+ // Verified before it is believed, and cached, since this is asked once per request.
335
+ let machine = (await getTrustedMachines()).find(entry => entry.machineId === machineId);
315
336
  if (!machine) {
316
- return false;
337
+ return { accepted: false, reason: `that machine is not trusted. ${addMachineCommand({ machineId, ip, domain })}` };
317
338
  }
318
339
 
319
340
  await syncRevocations(sourceURL);
320
341
  let unrevokes = await readUnrevokes(sourceURL).catch(() => ({ pairs: new Map(), legacyIds: new Map() }));
321
- // An unrevoke is only honoured once it has waited out its hour, exactly as an ssh key's is.
322
- let allowedAgain = async (pair: string) => {
323
- let unrevokeId = unrevokes.pairs.get(pair);
324
- return !!unrevokeId && await unrevokeInEffect(unrevokeId);
325
- };
342
+ // An unrevoke counts as soon as it is seen. It is signed, so writing one already takes the
343
+ // hardware key, and anyone holding that can sign anything at all - a wait before honouring it
344
+ // guards against an attacker who has no need of it.
345
+ let allowedAgain = (pair: string) => !!unrevokes.pairs.get(pair)?.length;
326
346
  let revocations = await readMachineRevocations(sourceURL);
327
347
  // Any revocation nothing has undone keeps the machine out, from everywhere, the way a revoked
328
348
  // ssh key is out everywhere rather than only from the address it was misused from.
329
- let revoked = false;
349
+ let frozenFrom = "";
330
350
  for (let revocation of revocations) {
331
351
  if (revocation.machineId !== machineId) {
332
352
  continue;
333
353
  }
334
- if (!await allowedAgain(pairKey({ fingerprint: revocation.machineId, ip: revocation.ip }))) {
335
- revoked = true;
354
+ if (!allowedAgain(pairKey({ fingerprint: revocation.machineId, ip: revocation.ip }))) {
355
+ frozenFrom = revocation.ip;
336
356
  }
337
357
  }
338
- if (revoked) {
339
- return false;
358
+ if (frozenFrom) {
359
+ return {
360
+ accepted: false,
361
+ reason: `that machine is frozen, it was used from ${frozenFrom} which it is not allowed from`,
362
+ };
340
363
  }
341
364
 
342
365
  if (machine.ips.includes(ip)) {
343
- return true;
366
+ return { accepted: true, reason: "" };
344
367
  }
345
368
 
346
369
  // Listed, but talking to us from somewhere it should not be. Recorded once for this machine
@@ -348,8 +371,9 @@ export async function isMachineAccepted(config: { machineId: string; ip: string
348
371
  let pair = pairKey({ fingerprint: machineId, ip });
349
372
  let alreadyRecorded = revocations.some(revocation =>
350
373
  pairKey({ fingerprint: revocation.machineId, ip: revocation.ip }) === pair);
351
- if (!alreadyRecorded && !await allowedAgain(pair)) {
374
+ if (!alreadyRecorded && !allowedAgain(pair)) {
352
375
  await recordMachineRevocation({ sourceURL, machineId, ip, hostLabel: os.hostname() });
376
+ return { accepted: false, reason: `that machine is not allowed from ${ip}, so it is now frozen everywhere` };
353
377
  }
354
- return false;
378
+ return { accepted: false, reason: `that machine is not allowed from ${ip}` };
355
379
  }
@@ -47,14 +47,16 @@ export function startTrustServer(config: { port: number }) {
47
47
  // Not a rejection of the machine, a packet that does not prove anything about one.
48
48
  return { status: 400, body: { trusted: false, ip, reason: verified.problem } };
49
49
  }
50
- let trusted = await isMachineAccepted({ machineId: packet.machineId, ip });
50
+ let verdict = await isMachineAccepted({ machineId: packet.machineId, ip });
51
51
  return {
52
52
  status: 200,
53
53
  body: {
54
- trusted,
54
+ trusted: verdict.accepted,
55
55
  machineId: packet.machineId,
56
56
  ip,
57
- reason: trusted && "" || "that machine is not trusted from that address",
57
+ // Whatever the check itself said. It is the only thing that knows which of the
58
+ // ways to be refused this was.
59
+ reason: verdict.reason,
58
60
  },
59
61
  };
60
62
  };
@@ -0,0 +1,36 @@
1
+ export declare const DEFAULT_WEBHOOK_FILE_PATH = "/etc/portsecure/discord-webhook";
2
+ /** Pulls the webhook URL out of webhook file contents. `sourceName` only appears in errors, so
3
+ callers can name a remote path. */
4
+ export declare function parseWebhookFile(config: {
5
+ contents: string;
6
+ sourceName: string;
7
+ }): string;
8
+ /** Keeps the id and both ends of the token, so a reader can confirm which webhook replaced theirs
9
+ without receiving one they could post to. */
10
+ export declare function redactWebhookURL(webhookURL: string): string;
11
+ export declare function formatWebhookFile(webhookURL: string): string;
12
+ /** When the message was sent, in the sending machine's own time zone. Discord shows when it
13
+ received something, which is not the same thing when a machine has been offline or a send has
14
+ been retried, and the zone matters when the machines are not all in one place. */
15
+ export declare function messageTimestamp(now: Date): string;
16
+ /** Sends to an explicit webhook URL, for tooling that acts on a webhook before (or instead of)
17
+ configureDiscordNotifications - setup, migrations, connectivity checks. */
18
+ export declare function sendToWebhookURL(config: {
19
+ webhookURL: string;
20
+ message: string;
21
+ }): Promise<void>;
22
+ /** Must be called once on startup, before any notification is sent. Aborts the process if the
23
+ webhook file is missing or invalid, then re-checks the file on an interval and warns the old
24
+ webhook whenever it changes. */
25
+ export declare function configureDiscordNotifications(config?: {
26
+ filePath?: string;
27
+ checkInterval?: number;
28
+ }): Promise<{
29
+ filePath: string;
30
+ checkInterval: number;
31
+ }>;
32
+ export declare function sendDiscordNotification(message: string): Promise<void>;
33
+ /** Whether notifications are set up in this process. For code that may run outside the daemon,
34
+ where sending is worth doing if it can be and not worth failing over if it cannot. */
35
+ export declare function areDiscordNotificationsConfigured(): boolean;
36
+ export declare function stopDiscordNotifications(): void;
@@ -1,4 +1,4 @@
1
- "use strict";
1
+ "use strict"; var console = (function shimConsole() { let baseConsole = globalThis.console; if (typeof window !== "undefined") return baseConsole; let name = __filename.replaceAll("\\", "/").split("/").pop(); let dirName = __dirname.replaceAll("\\", "/").split("/").pop(); let context = { __FILE__: __filename, __DIR__: __dirname, __NAME__: name, __DIRNAME__: dirName }; let newConsole = { ...baseConsole }; for (let shim of ["log", "info", "warn", "error"]) { newConsole[shim] = (...args) => { args.push(context); baseConsole[shim](...args); }; } newConsole.__context = context; return newConsole; })(); "use strict";
2
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
4
4
  };
@@ -203,4 +203,4 @@ function stopDiscordNotifications() {
203
203
  }
204
204
  exports.stopDiscordNotifications = stopDiscordNotifications;
205
205
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlzY29yZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImRpc2NvcmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBRTdCLHNIQUFzSDtBQUN0SCxrR0FBa0c7QUFDbEcsOENBQThDO0FBRWpDLFFBQUEseUJBQXlCLEdBQUcsaUNBQWlDLENBQUM7QUFDM0UsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQztBQUM3QyxNQUFNLHNCQUFzQixHQUFHO0lBQzNCLG1DQUFtQztJQUNuQyxzQ0FBc0M7SUFDdEMsMENBQTBDO0NBQzdDLENBQUM7QUFDRixNQUFNLHFCQUFxQixHQUFHLElBQUksQ0FBQztBQUNuQyxNQUFNLHFCQUFxQixHQUFHLEdBQUcsQ0FBQztBQUNsQyxNQUFNLGlCQUFpQixHQUFHLENBQUMsQ0FBQztBQUM1QixNQUFNLHNCQUFzQixHQUFHLENBQUMsQ0FBQztBQUNqQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7QUFFekMsSUFBSSxpQkFJUyxDQUFDO0FBRWQsc0ZBQXNGO0FBQ3RGLDRDQUE0QztBQUM1QyxJQUFJLFNBQVMsR0FBcUIsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO0FBRXBEO3NDQUNzQztBQUN0QyxTQUFnQixnQkFBZ0IsQ0FBQyxNQUFnRDtJQUM3RSxJQUFJLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUN0QyxJQUFJLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUN4RyxJQUFJLFVBQVUsR0FBRyxL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Q29udGVudCI6WyJpbXBvcnQgZnMgZnJvbSBcImZzL3Byb21pc2VzXCI7XG5cbi8vIFBPUlRFRCBDT0RFOiBzZWN1cml0eS9hdXRob3JpemVkS2V5cy9kYWVtb24vcG9ydHNlY3VyZURhZW1vbi5qcyBlbWJlZHMgYSBoYW5kIHBvcnQgb2YgdGhpcyBmaWxlIGluIHBsYWluIEpTLCBzbyB0aGVcbi8vIGRhZW1vbiBjYW4gcnVuIHdpdGggbm8gZGVwZW5kZW5jaWVzLiBUaGUgdHdvIGFyZSBleHBlY3RlZCB0byBiZWhhdmUgaWRlbnRpY2FsbHkgLSBpZiB5b3UgY2hhbmdlXG4vLyBvbmUsIG1ha2UgdGhlIG1hdGNoaW5nIGNoYW5nZSBpbiB0aGUgb3RoZXIuXG5cbmV4cG9ydCBjb25zdCBERUZBVUxUX1dFQkhPT0tfRklMRV9QQVRIID0gXCIvZXRjL3BvcnRzZWN1cmUvZGlzY29yZC13ZWJob29rXCI7XG5jb25zdCBERUZBVUxUX0NIRUNLX0lOVEVSVkFMID0gNSAqIDYwICogMTAwMDtcbmNvbnN0IFZBTElEX1dFQkhPT0tfUFJFRklYRVMgPSBbXG4gICAgXCJodHRwczovL2Rpc2NvcmQuY29tL2FwaS93ZWJob29rcy9cIixcbiAgICBcImh0dHBzOi8vZGlzY29yZGFwcC5jb20vYXBpL3dlYmhvb2tzL1wiLFxuICAgIFwiaHR0cHM6Ly9jYW5hcnkuZGlzY29yZC5jb20vYXBpL3dlYmhvb2tzL1wiLFxuXTtcbmNvbnN0IERJU0NPUkRfTUVTU0FHRV9MSU1JVCA9IDIwMDA7XG5jb25zdCBNQVhfRVJST1JfQk9EWV9MRU5HVEggPSA1MDA7XG5jb25zdCBNQVhfU0VORF9BVFRFTVBUUyA9IDM7XG5jb25zdCBSRURBQ1RFRF9UT0tFTl9WSVNJQkxFID0gODtcbmNvbnN0IERFRkFVTFRfUkFURV9MSU1JVF9XQUlUID0gMiAqIDEwMDA7XG5cbmxldCBub3RpZmljYXRpb25TdGF0ZToge1xuICAgIGZpbGVQYXRoOiBzdHJpbmc7XG4gICAgd2ViaG9va1VSTDogc3RyaW5nO1xuICAgIGNoZWNrVGltZXI6IE5vZGVKUy5UaW1lb3V0O1xufSB8IHVuZGVmaW5lZDtcblxuLy8gRGlzY29yZCB3ZWJob29rcyBhcmUgcmF0ZSBsaW1pdGVkIChyb3VnaGx5IDUgcmVxdWVzdHMgcGVyIDIgc2Vjb25kcyksIHNvIGV2ZXJ5IHNlbmRcbi8vIGdvZXMgdGhyb3VnaCBvbmUgY2hhaW4gaW5zdGVhZCBvZiByYWNpbmcuXG5sZXQgc2VuZENoYWluOiBQcm9taXNlPHVua25vd24+ID0gUHJvbWlzZS5yZXNvbHZlKCk7XG5cbi8qKiBQdWxscyB0aGUgd2ViaG9vayBVUkwgb3V0IG9mIHdlYmhvb2sgZmlsZSBjb250ZW50cy4gYHNvdXJjZU5hbWVgIG9ubHkgYXBwZWFycyBpbiBlcnJvcnMsIHNvXG4gICAgY2FsbGVycyBjYW4gbmFtZSBhIHJlbW90ZSBwYXRoLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHBhcnNlV2ViaG9va0ZpbGUoY29uZmlnOiB7IGNvbnRlbnRzOiBzdHJpbmc7IHNvdXJjZU5hbWU6IHN0cmluZyB9KSB7XG4gICAgbGV0IHsgY29udGVudHMsIHNvdXJjZU5hbWUgfSA9IGNvbmZpZztcbiAgICBsZXQgbGluZXMgPSBjb250ZW50cy5zcGxpdChcIlxcblwiKS5tYXAobGluZSA9PiBsaW5lLnRyaW0oKSkuZmlsdGVyKGxpbmUgPT4gbGluZSAmJiAhbGluZS5zdGFydHNXaXRoKFwiI1wiKSk7XG4gICAgbGV0IHdlYmhvb2tVUkwgPSBsaW5lc1swXTtcbiAgICBpZiAoIXdlYmhvb2tVUkwpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBhIERpc2NvcmQgd2ViaG9vayBVUkwgaW4gJHtzb3VyY2VOYW1lfSwgdGhlIGZpbGUgaGFzIG5vIHVzYWJsZSBsaW5lc2ApO1xuICAgIH1cbiAgICBpZiAoIVZBTElEX1dFQkhPT0tfUFJFRklYRVMuc29tZShwcmVmaXggPT4gd2ViaG9va1VSTC5zdGFydHNXaXRoKHByZWZpeCkpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCBhIERpc2NvcmQgd2ViaG9vayBVUkwgc3RhcnRpbmcgd2l0aCBvbmUgb2YgJHtWQUxJRF9XRUJIT09LX1BSRUZJWEVTLmpvaW4oXCIsIFwiKX0sIGBcbiAgICAgICAgICAgICsgYHdhcyAke3dlYmhvb2tVUkwuc2xpY2UoMCwgTUFYX0VSUk9SX0JPRFlfTEVOR1RIKX0gKGluICR7c291cmNlTmFtZX0pYFxuICAgICAgICApO1xuICAgIH1cbiAgICByZXR1cm4gd2ViaG9va1VSTDtcbn1cblxuLyoqIEtlZXBzIHRoZSBpZCBhbmQgYm90aCBlbmRzIG9mIHRoZSB0b2tlbiwgc28gYSByZWFkZXIgY2FuIGNvbmZpcm0gd2hpY2ggd2ViaG9vayByZXBsYWNlZCB0aGVpcnNcbiAgICB3aXRob3V0IHJlY2VpdmluZyBvbmUgdGhleSBjb3VsZCBwb3N0IHRvLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlZGFjdFdlYmhvb2tVUkwod2ViaG9va1VSTDogc3RyaW5nKSB7XG4gICAgbGV0IHNlcGFyYXRvckluZGV4ID0gd2ViaG9va1VSTC5sYXN0SW5kZXhPZihcIi9cIik7XG4gICAgbGV0IGJhc2UgPSB3ZWJob29rVVJMLnNsaWNlKDAsIHNlcGFyYXRvckluZGV4ICsgMSk7XG4gICAgbGV0IHRva2VuID0gd2ViaG9va1VSTC5zbGljZShzZXBhcmF0b3JJbmRleCArIDEpO1xuICAgIGlmICh0b2tlbi5sZW5ndGggPD0gUkVEQUNURURfVE9LRU5fVklTSUJMRSAqIDIpIHtcbiAgICAgICAgLy8gVG9vIHNob3J0IHRvIHNob3cgYm90aCBlbmRzIHdpdGhvdXQgZ2l2aW5nIGF3YXkgdGhlIHdob2xlIHRva2VuLlxuICAgICAgICByZXR1cm4gYCR7YmFzZX0ke3Rva2VuLnNsaWNlKDAsIFJFREFDVEVEX1RPS0VOX1ZJU0lCTEUpfS4uLmA7XG4gICAgfVxuICAgIHJldHVybiBgJHtiYXNlfSR7dG9rZW4uc2xpY2UoMCwgUkVEQUNURURfVE9LRU5fVklTSUJMRSl9Li4uJHt0b2tlbi5zbGljZSgtUkVEQUNURURfVE9LRU5fVklTSUJMRSl9YDtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGZvcm1hdFdlYmhvb2tGaWxlKHdlYmhvb2tVUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgIyBwb3J0c2VjdXJlIERpc2NvcmQgd2ViaG9vay4gRmlyc3Qgbm9uLWNvbW1lbnQgbGluZSBpcyB1c2VkLlxcbiR7d2ViaG9va1VSTH1cXG5gO1xufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkV2ViaG9va0ZpbGUoZmlsZVBhdGg6IHN0cmluZykge1xuICAgIGxldCBjb250ZW50cztcbiAgICB0cnkge1xuICAgICAgICBjb250ZW50cyA9IGF3YWl0IGZzLnJlYWRGaWxlKGZpbGVQYXRoLCBcInV0ZjhcIik7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEV4cGVjdGVkIGEgcmVhZGFibGUgRGlzY29yZCB3ZWJob29rIGZpbGUgYXQgJHtmaWxlUGF0aH0sICR7ZX1gKTtcbiAgICB9XG4gICAgcmV0dXJuIHBhcnNlV2ViaG9va0ZpbGUoeyBjb250ZW50cywgc291cmNlTmFtZTogZmlsZVBhdGggfSk7XG59XG5cbi8qKiBXaGVuIHRoZSBtZXNzYWdlIHdhcyBzZW50LCBpbiB0aGUgc2VuZGluZyBtYWNoaW5lJ3Mgb3duIHRpbWUgem9uZS4gRGlzY29yZCBzaG93cyB3aGVuIGl0XG4gICAgcmVjZWl2ZWQgc29tZXRoaW5nLCB3aGljaCBpcyBub3QgdGhlIHNhbWUgdGhpbmcgd2hlbiBhIG1hY2hpbmUgaGFzIGJlZW4gb2ZmbGluZSBvciBhIHNlbmQgaGFzXG4gICAgYmVlbiByZXRyaWVkLCBhbmQgdGhlIHpvbmUgbWF0dGVycyB3aGVuIHRoZSBtYWNoaW5lcyBhcmUgbm90IGFsbCBpbiBvbmUgcGxhY2UuICovXG5leHBvcnQgZnVuY3Rpb24gbWVzc2FnZVRpbWVzdGFtcChub3c6IERhdGUpIHtcbiAgICBsZXQgZGF0ZSA9IG5vdy50b0xvY2FsZURhdGVTdHJpbmcoXCJlbi1DQVwiKTtcbiAgICBsZXQgdGltZSA9IG5vdy50b0xvY2FsZVRpbWVTdHJpbmcoXCJlbi1VU1wiLCB7XG4gICAgICAgIGhvdXI6IFwiMi1kaWdpdFwiLFxuICAgICAgICBtaW51dGU6IFwiMi1kaWdpdFwiLFxuICAgICAgICBzZWNvbmQ6IFwiMi1kaWdpdFwiLFxuICAgICAgICBob3VyMTI6IHRydWUsXG4gICAgICAgIHRpbWVab25lTmFtZTogXCJzaG9ydFwiLFxuICAgIH0pO1xuICAgIHJldHVybiBgJHtkYXRlfSAke3RpbWV9YDtcbn1cblxuYXN5bmMgZnVuY3Rpb24gcG9zdFRvV2ViaG9vayh3ZWJob29rVVJMOiBzdHJpbmcsIG1lc3NhZ2U6IHN0cmluZykge1xuICAgIC8vIFN0YW1wZWQgb25jZSByYXRoZXIgdGhhbiBwZXIgYXR0ZW1wdCwgc28gYSByZXRyeSBzYXlzIHdoZW4gdGhlIHRoaW5nIGhhcHBlbmVkIHJhdGhlciB0aGFuXG4gICAgLy8gd2hlbiB3ZSBsYXN0IG1hbmFnZWQgdG8gZ2V0IGl0IG91dC4gQXQgdGhlIGVuZCwgYmVjYXVzZSB0aGUgZmlyc3QgbGluZSBvZiBhIG1lc3NhZ2UgaXMgdGhlXG4gICAgLy8gbm90aWZpY2F0aW9uIHByZXZpZXcgYW5kIGEgdGltZXN0YW1wIHRoZXJlIHdvdWxkIHNwZW5kIGl0IHNheWluZyBub3RoaW5nLlxuICAgIGxldCBjb250ZW50ID0gYCR7bWVzc2FnZX0gwrcgXFxgJHttZXNzYWdlVGltZXN0YW1wKG5ldyBEYXRlKCkpfVxcYGA7XG4gICAgaWYgKGNvbnRlbnQubGVuZ3RoID4gRElTQ09SRF9NRVNTQUdFX0xJTUlUKSB7XG4gICAgICAgIGNvbnRlbnQgPSBjb250ZW50LnNsaWNlKDAsIERJU0NPUkRfTUVTU0FHRV9MSU1JVCAtIDMpICsgXCIuLi5cIjtcbiAgICB9XG4gICAgZm9yIChsZXQgYXR0ZW1wdCA9IDE7IGF0dGVtcHQgPD0gTUFYX1NFTkRfQVRURU1QVFM7IGF0dGVtcHQrKykge1xuICAgICAgICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmZXRjaCh3ZWJob29rVVJMLCB7XG4gICAgICAgICAgICBtZXRob2Q6IFwiUE9TVFwiLFxuICAgICAgICAgICAgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9LFxuICAgICAgICAgICAgYm9keTogSlNPTi5zdHJpbmdpZnkoeyBjb250ZW50IH0pLFxuICAgICAgICB9KTtcbiAgICAgICAgaWYgKHJlc3BvbnNlLm9rKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgbGV0IGJvZHkgPSAoYXdhaXQgcmVzcG9uc2UudGV4dCgpKS5zbGljZSgwLCBNQVhfRVJST1JfQk9EWV9MRU5HVEgpO1xuICAgICAgICBpZiAocmVzcG9uc2Uuc3RhdHVzID09PSA0MjkgJiYgYXR0ZW1wdCA8IE1BWF9TRU5EX0FUVEVNUFRTKSB7XG4gICAgICAgICAgICBsZXQgcmV0cnlBZnRlciA9IE51bWJlcihyZXNwb25zZS5oZWFkZXJzLmdldChcInJldHJ5LWFmdGVyXCIpKTtcbiAgICAgICAgICAgIGxldCB3YWl0VGltZSA9IHJldHJ5QWZ0ZXIgJiYgcmV0cnlBZnRlciAqIDEwMDAgfHwgREVGQVVMVF9SQVRFX0xJTUlUX1dBSVQ7XG4gICAgICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZShyZXNvbHZlID0+IHNldFRpbWVvdXQocmVzb2x2ZSwgd2FpdFRpbWUpKTtcbiAgICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihcbiAgICAgICAgICAgIGBFeHBlY3RlZCBhIDJ4eCByZXNwb25zZSBmcm9tIHRoZSBEaXNjb3JkIHdlYmhvb2ssIHdhcyAke3Jlc3BvbnNlLnN0YXR1c30gJHtyZXNwb25zZS5zdGF0dXNUZXh0fSwgYm9keSAke2JvZHl9YFxuICAgICAgICApO1xuICAgIH1cbn1cblxuZnVuY3Rpb24gcXVldWVTZW5kKHdlYmhvb2tVUkw6IHN0cmluZywgbWVzc2FnZTogc3RyaW5nKSB7XG4gICAgbGV0IHNlbmQgPSAoKSA9PiBwb3N0VG9XZWJob29rKHdlYmhvb2tVUkwsIG1lc3NhZ2UpO1xuICAgIGxldCByZXN1bHQgPSBzZW5kQ2hhaW4udGhlbihzZW5kLCBzZW5kKTtcbiAgICBzZW5kQ2hhaW4gPSByZXN1bHQuY2F0Y2goKCkgPT4gdW5kZWZpbmVkKTtcbiAgICByZXR1cm4gcmVzdWx0O1xufVxuXG4vKiogU2VuZHMgdG8gYW4gZXhwbGljaXQgd2ViaG9vayBVUkwsIGZvciB0b29saW5nIHRoYXQgYWN0cyBvbiBhIHdlYmhvb2sgYmVmb3JlIChvciBpbnN0ZWFkIG9mKVxuICAgIGNvbmZpZ3VyZURpc2NvcmROb3RpZmljYXRpb25zIC0gc2V0dXAsIG1pZ3JhdGlvbnMsIGNvbm5lY3Rpdml0eSBjaGVja3MuICovXG5leHBvcnQgZnVuY3Rpb24gc2VuZFRvV2ViaG9va1VSTChjb25maWc6IHsgd2ViaG9va1VSTDogc3RyaW5nOyBtZXNzYWdlOiBzdHJpbmcgfSkge1xuICAgIHJldHVybiBxdWV1ZVNlbmQoY29uZmlnLndlYmhvb2tVUkwsIGNvbmZpZy5tZXNzYWdlKTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gY2hlY2tXZWJob29rRmlsZUNoYW5nZWQoKSB7XG4gICAgbGV0IHN0YXRlID0gbm90aWZpY2F0aW9uU3RhdGU7XG4gICAgaWYgKCFzdGF0ZSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGxldCBuZXdXZWJob29rVVJMO1xuICAgIHRyeSB7XG4gICAgICAgIG5ld1dlYmhvb2tVUkwgPSBhd2FpdCByZWFkV2ViaG9va0ZpbGUoc3RhdGUuZmlsZVBhdGgpO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogRGlzY29yZCB3ZWJob29rIGZpbGUgaXMgbm8gbG9uZ2VyIHJlYWRhYmxlLCBzdGlsbCB1c2luZyB0aGUgbG9hZGVkIHdlYmhvb2suICR7ZX1gKTtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAobmV3V2ViaG9va1VSTCA9PT0gc3RhdGUud2ViaG9va1VSTCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIC8vIFdhcm4gdGhlIG9sZCBjaGFubmVsIGZpcnN0LCB3aXRoIHRoZSBuZXcgd2ViaG9vayByZWRhY3RlZCwgc28gYSBzdG9sZW4gb2xkIHdlYmhvb2sgZG9lcyBub3RcbiAgICAvLyBoYW5kIHRoZSBhdHRhY2tlciBhIHVzYWJsZSBuZXcgb25lLlxuICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IHF1ZXVlU2VuZChcbiAgICAgICAgICAgIHN0YXRlLndlYmhvb2tVUkwsXG4gICAgICAgICAgICBgKipwb3J0c2VjdXJlKio6ICoqTk9USUZJQ0FUSU9OUyBBUkUgTU9WSU5HIFRPIEFOT1RIRVIgQ0hBTk5FTCoqYFxuICAgICAgICAgICAgKyBgXFxuXFxuVGhpcyBjaGFubmVsIHN0b3BzIHJlY2VpdmluZyB0aGVtIG5vdy4gSWYgdGhhdCB3YXMgbm90IHlvdSwgc29tZWJvZHkgd2l0aCBhY2Nlc3NgXG4gICAgICAgICAgICArIGAgdG8gdGhpcyBtYWNoaW5lIGp1c3QgcmVkaXJlY3RlZCBpdHMgYWxlcnRzLmBcbiAgICAgICAgICAgICsgYFxcblxcbm5ldyB3ZWJob29rOiBcXGAke3JlZGFjdFdlYmhvb2tVUkwobmV3V2ViaG9va1VSTCl9XFxgYFxuICAgICAgICAgICAgKyBgXFxuc2V0IGluOiBcXGAke3N0YXRlLmZpbGVQYXRofVxcYGBcbiAgICAgICAgKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IoYHBvcnRzZWN1cmU6IGZhaWxlZCB0byB3YXJuIHRoZSBvbGQgRGlzY29yZCB3ZWJob29rIGFib3V0IHRoZSBjaGFuZ2UuICR7ZX1gKTtcbiAgICB9XG4gICAgc3RhdGUud2ViaG9va1VSTCA9IG5ld1dlYmhvb2tVUkw7XG59XG5cbi8qKiBNdXN0IGJlIGNhbGxlZCBvbmNlIG9uIHN0YXJ0dXAsIGJlZm9yZSBhbnkgbm90aWZpY2F0aW9uIGlzIHNlbnQuIEFib3J0cyB0aGUgcHJvY2VzcyBpZiB0aGVcbiAgICB3ZWJob29rIGZpbGUgaXMgbWlzc2luZyBvciBpbnZhbGlkLCB0aGVuIHJlLWNoZWNrcyB0aGUgZmlsZSBvbiBhbiBpbnRlcnZhbCBhbmQgd2FybnMgdGhlIG9sZFxuICAgIHdlYmhvb2sgd2hlbmV2ZXIgaXQgY2hhbmdlcy4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBjb25maWd1cmVEaXNjb3JkTm90aWZpY2F0aW9ucyhjb25maWc/OiB7XG4gICAgZmlsZVBhdGg/OiBzdHJpbmc7XG4gICAgY2hlY2tJbnRlcnZhbD86IG51bWJlcjtcbn0pIHtcbiAgICBpZiAobm90aWZpY2F0aW9uU3RhdGUpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBjb25maWd1cmVEaXNjb3JkTm90aWZpY2F0aW9ucyB0byBiZSBjYWxsZWQgb25jZSwgd2FzIGNhbGxlZCBhZ2FpbiAoYWxyZWFkeSB1c2luZyAke25vdGlmaWNhdGlvblN0YXRlLmZpbGVQYXRofSlgKTtcbiAgICB9XG4gICAgbGV0IGZpbGVQYXRoID0gY29uZmlnPy5maWxlUGF0aCB8fCBERUZBVUxUX1dFQkhPT0tfRklMRV9QQVRIO1xuICAgIGxldCBjaGVja0ludGVydmFsID0gY29uZmlnPy5jaGVja0ludGVydmFsIHx8IERFRkFVTFRfQ0hFQ0tfSU5URVJWQUw7XG4gICAgbGV0IHdlYmhvb2tVUkw7XG4gICAgdHJ5IHtcbiAgICAgICAgd2ViaG9va1VSTCA9IGF3YWl0IHJlYWRXZWJob29rRmlsZShmaWxlUGF0aCk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjb25zb2xlLmVycm9yKGBwb3J0c2VjdXJlOiByZWZ1c2luZyB0byBzdGFydCB3aXRob3V0IGEgdmFsaWQgRGlzY29yZCB3ZWJob29rIGZpbGUuXFxuJHtlfWApO1xuICAgICAgICBwcm9jZXNzLmV4aXQoMSk7XG4gICAgfVxuICAgIGxldCBjaGVja1RpbWVyID0gc2V0SW50ZXJ2YWwoKCkgPT4ge1xuICAgICAgICBjaGVja1dlYmhvb2tGaWxlQ2hhbmdlZCgpLmNhdGNoKGUgPT4gY29uc29sZS5lcnJvcihgcG9ydHNlY3VyZTogRGlzY29yZCB3ZWJob29rIGZpbGUgY2hlY2sgZmFpbGVkLiAke2V9YCkpO1xuICAgIH0sIGNoZWNrSW50ZXJ2YWwpO1xuICAgIC8vIFRoZSBjaGVjayBzaG91bGQgbmV2ZXIgYmUgdGhlIHJlYXNvbiB0aGUgcHJvY2VzcyBzdGF5cyBhbGl2ZS5cbiAgICBjaGVja1RpbWVyLnVucmVmKCk7XG4gICAgbm90aWZpY2F0aW9uU3RhdGUgPSB7IGZpbGVQYXRoLCB3ZWJob29rVVJMLCBjaGVja1RpbWVyIH07XG4gICAgcmV0dXJuIHsgZmlsZVBhdGgsIGNoZWNrSW50ZXJ2YWwgfTtcbn1cblxuLy8gRE8gTk9UIGFkZCBuZXcgY2FsbHMgdG8gdGhpcy4gRXZlcnkgbWVzc2FnZSBnb2VzIHRvIGEgcmVhbCBEaXNjb3JkIHNlcnZlciBzb21lb25lIHJlYWRzLCBzbyBhXG4vLyBub3RpZmljYXRpb24gaXMgb25seSBldmVyIGFkZGVkIHdoZW4gdGhlIHVzZXIgZXhwbGljaXRseSBhc2tzIGZvciB0aGF0IHNwZWNpZmljIGNhc2UuIFN0YXJ0dXAsXG4vLyBzdWNjZXNzLCBlcnJvcnMsIHJldHJpZXMgYW5kIHJlY292ZXJpZXMgYWxsIGJlbG9uZyBpbiBhIGxvZyBpbnN0ZWFkLlxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHNlbmREaXNjb3JkTm90aWZpY2F0aW9uKG1lc3NhZ2U6IHN0cmluZykge1xuICAgIGxldCBzdGF0ZSA9IG5vdGlmaWNhdGlvblN0YXRlO1xuICAgIGlmICghc3RhdGUpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCBjb25maWd1cmVEaXNjb3JkTm90aWZpY2F0aW9ucyB0byBiZSBjYWxsZWQgYmVmb3JlIHNlbmRpbmcsIHdhcyBjYWxsZWQgd2l0aCBtZXNzYWdlICR7bWVzc2FnZS5zbGljZSgwLCBNQVhfRVJST1JfQk9EWV9MRU5HVEgpfWApO1xuICAgIH1cbiAgICBhd2FpdCBxdWV1ZVNlbmQoc3RhdGUud2ViaG9va1VSTCwgbWVzc2FnZSk7XG59XG5cbi8qKiBXaGV0aGVyIG5vdGlmaWNhdGlvbnMgYXJlIHNldCB1cCBpbiB0aGlzIHByb2Nlc3MuIEZvciBjb2RlIHRoYXQgbWF5IHJ1biBvdXRzaWRlIHRoZSBkYWVtb24sXG4gICAgd2hlcmUgc2VuZGluZyBpcyB3b3J0aCBkb2luZyBpZiBpdCBjYW4gYmUgYW5kIG5vdCB3b3J0aCBmYWlsaW5nIG92ZXIgaWYgaXQgY2Fubm90LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFyZURpc2NvcmROb3RpZmljYXRpb25zQ29uZmlndXJlZCgpIHtcbiAgICByZXR1cm4gISFub3RpZmljYXRpb25TdGF0ZTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BEaXNjb3JkTm90aWZpY2F0aW9ucygpIHtcbiAgICBpZiAoIW5vdGlmaWNhdGlvblN0YXRlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY2xlYXJJbnRlcnZhbChub3RpZmljYXRpb25TdGF0ZS5jaGVja1RpbWVyKTtcbiAgICBub3RpZmljYXRpb25TdGF0ZSA9IHVuZGVmaW5lZDtcbn1cbiJdfQ==
206
- /* _JS_SOURCE_HASH = "abb6e0b243d1d85cb4a3599020263d0b3164ce4bf2aef2aa81489e304cd43b4c"; */
206
+ /* _JS_SOURCE_HASH = "a4fe18f537fa6da028d1607b16331b146697f9fabb96c6de058f0b41c5553b17"; */
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
3
+ //exports.ensureRemoteWebhook = exports.REPLACE_KEYWORD = void 0;
4
+ const discord_1 = require("./discord");
5
+ const remoteSSH_1 = require("../helpers/remoteSSH");
6
+ exports.REPLACE_KEYWORD = "replace";
7
+ const UNPARSEABLE_LABEL = "(unparseable)";
8
+ /** Puts a webhook on a remote host, refusing to clobber a different existing one unless asked.
9
+ Returns what happened, so callers can decide whether to keep going. */
10
+ async function ensureRemoteWebhook(config) {
11
+ let { host, webhookURL, replace } = config;
12
+ let filePath = config.filePath || discord_1.DEFAULT_WEBHOOK_FILE_PATH;
13
+ let replacedURL = "";
14
+ let existingContents = await (0, remoteSSH_1.readRemoteFile)({ host, filePath });
15
+ if (existingContents) {
16
+ // A corrupt existing file still counts as a conflict, so "replace" can repair it instead
17
+ // of the setup dead ending on a parse error.
18
+ let existingURL = UNPARSEABLE_LABEL;
19
+ try {
20
+ existingURL = (0, discord_1.parseWebhookFile)({ contents: existingContents, sourceName: `${host}:${filePath}` });
21
+ }
22
+ catch (e) {
23
+ console.error(`${host}:${filePath} exists but could not be parsed. ${e}`);
24
+ }
25
+ if (existingURL === webhookURL) {
26
+ return { outcome: "unchanged", filePath };
27
+ }
28
+ if (!replace) {
29
+ throw new Error(`Expected no conflicting webhook on ${host}, but ${filePath} already holds a different one.\n`
30
+ + `Existing: ${existingURL}\n`
31
+ + `New: ${webhookURL}\n`
32
+ + `Pass "${exports.REPLACE_KEYWORD}" to overwrite it.`);
33
+ }
34
+ replacedURL = existingURL;
35
+ }
36
+ await (0, remoteSSH_1.writeRemoteFile)({
37
+ host,
38
+ filePath,
39
+ contents: (0, discord_1.formatWebhookFile)(webhookURL),
40
+ fileMode: "600",
41
+ directoryMode: "700",
42
+ });
43
+ let writtenContents = await (0, remoteSSH_1.readRemoteFile)({ host, filePath });
44
+ if (!writtenContents) {
45
+ throw new Error(`Expected ${filePath} to exist on ${host} after writing, no such file exists`);
46
+ }
47
+ let writtenURL = (0, discord_1.parseWebhookFile)({ contents: writtenContents, sourceName: `${host}:${filePath}` });
48
+ if (writtenURL !== webhookURL) {
49
+ throw new Error(`Expected ${host}:${filePath} to hold the new webhook, was ${writtenURL}`);
50
+ }
51
+ if (replacedURL && replacedURL !== UNPARSEABLE_LABEL) {
52
+ // The new webhook is redacted, so this channel can identify the replacement without
53
+ // receiving a webhook it could post to.
54
+ try {
55
+ await (0, discord_1.sendToWebhookURL)({
56
+ webhookURL: replacedURL,
57
+ message: `**portsecure**: the Discord webhook for \`${host}\` was replaced with`
58
+ + ` \`${(0, discord_1.redactWebhookURL)(webhookURL)}\`.`
59
+ + ` This channel will stop receiving notifications for that host.`,
60
+ });
61
+ }
62
+ catch (e) {
63
+ // The replacement already happened, so a dead old webhook must not fail the setup.
64
+ console.error(`Could not notify the old webhook that it was replaced. ${e}`);
65
+ }
66
+ }
67
+ // Failing here means the file is in place but the webhook itself does not work, which is
68
+ // exactly what the operator needs to hear about.
69
+ await (0, discord_1.sendToWebhookURL)({
70
+ webhookURL,
71
+ message: `**portsecure**: notifications are now configured for \`${host}\`.`
72
+ + ` This channel will receive its security notifications.`,
73
+ });
74
+ return { outcome: replacedURL && "replaced" || "created", filePath };
75
+ }
76
+ exports.ensureRemoteWebhook = ensureRemoteWebhook;
77
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVtb3RlV2ViaG9vay5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJlbW90ZVdlYmhvb2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsdUNBQStIO0FBQy9ILG9EQUF1RTtBQUUxRCxRQUFBLGVBQWUsR0FBRyxTQUFTLENBQUM7QUFDekMsTUFBTSxpQkFBaUIsR0FBRyxlQUFlLENBQUM7QUFFMUM7MEVBQzBFO0FBQ25FLEtBQUssVUFBVSxtQkFBbUIsQ0FBQyxNQUt6QztJQUNHLElBQUksRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFLE9BQU8sRUFBRSxHQUFHLE1BQU0sQ0FBQztJQUMzQyxJQUFJLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxJQUFJLG1DQUF5QixDQUFDO0lBRTVELElBQUksV0FBVyxHQUFHLEVBQUUsQ0FBQztJQUNyQixJQUFJLGdCQUFnQixHQUFHLE1BQU0sSUFBQSwwQkFBYyxFQUFDLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDaEUsSUFBSSxnQkFBZ0IsRUFBRSxDQUFDO1FBQ25CLHlGQUF5RjtRQUN6Riw2Q0FBNkM7UUFDN0MsSUFBSSxXQUFXLEdBQUcsaUJBQWlCLENBQUM7UUFDcEMsSUFBSSxDQUFDO1lBQ0QsV0FBVyxHQUFHLElBQUEsMEJBQWdCLEVBQUMsRUFBRSxRQUFRLEVBQUUsZ0JBQWdCLEVBQUUsVUFBVSxFQUFFLEdBQUcsSUFBSSxJQUFJLFFBQVEsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUN0RyxDQUFDO1FBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNULE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxJQUFJLElBQUksUUFBUSxvQ0FBb0MsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUM5RSxDQUFDO1FBQ0QsSUFBSSxXQUFXLEtBQUssVUFBVSxFQUFFLENBQUM7WUFDN0IsT0FBTyxFQUFFLE9BQU8sRUFBRSxXQUFvQixFQUFFLFFBQVEsRUFBRSxDQUFDO1FBQ3ZELENBQUM7UUFDRCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDWCxNQUFNLElBQUksS0FBSyxDQUNYLHNDQUFzQyxJQUFJLFNBQVMsUUFBUSxtQ0FBbUM7a0JBQzVGLGFBQWEsV0FBVyxJQUFJO2tCQUM1QixhQUFhLFVBQVUsSUFBSTtrQkFDM0IsU0FBUyx1QkFBZSxvQkFBb0IsQ0FDakQsQ0FBQztRQUNOLENBQUM7UUFDRCxXQUFXLEdBQUcsV0FBVyxDQUFDO0lBQzlCLENBQUM7SUFFRCxNQUFNLElBQUEsMkJBQWUsRUFBQztRQUNsQixJQUFJO1FBQ0osUUFBUTtRQUNSLFFBQVEsRUFBRSxJQUFBLDJCQUFpQixFQUFDLFVBQVUsQ0FBQztRQUN2QyxRQUFRLEVBQUUsS0FBSztRQUNmLGFBQWEsRUFBRSxLQUFLO0tBQ3ZCLENBQUMsQ0FBQztJQUVILElBQUksZUFBZSxHQUFHLE1BQU0sSUFBQSwwQkFBYyxFQUFDLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDL0QsSUFBSSxDQUFDLGVBQWUsRUFBRSxDQUFDO1FBQ25CLE1BQU0sSUFBSSxLQUFLLENBQUMsWUFBWSxRQUFRLGdCQUFnQixJQUFJLHFDQUFxQyxDQUFDLENBQUM7SUFDbkcsQ0FBQztJQUNELElBQUksVUFBVSxHQUFHLElBQUEsMEJBQWdCLEVBQUMsRUFBRSxRQUFRLEVBQUUsZUFBZSxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksSUFBSSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEcsSUFBSSxVQUFVLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDNUIsTUFBTSxJQUFJLEtBQUssQ0FBQyxZQUFZLElBQUksSUFBSSxRQUFRLGlDQUFpQyxVQUFVLEVBQUUsQ0FBQyxDQUFDO0lBQy9GLENBQUM7SUFFRCxJQUFJLFdBQVcsSUFBSSxXQUFXLEtBQUssaUJBQWlCLEVBQUUsQ0FBQztRQUNuRCxvRkFBb0Y7UUFDcEYsd0NBQXdDO1FBQ3hDLElBQUksQ0FBQztZQUNELE1BQU0sSUFBQSwwQkFBZ0IsRUFBQztnQkFDbkIsVUFBVSxFQUFFLFdBQVc7Z0JBQ3ZCLE9BQU8sRUFBRSw2Q0FBNkMsSUFBSSxzQkFBc0I7c0JBQzFFLE1BQU0sSUFBQSwwQkFBZ0IsRUFBQyxVQUFVLENBQUMsS0FBSztzQkFDdkMsZ0VBQWdFO2FBQ3pFLENBQUMsQ0FBQztRQUNQLENBQUM7UUFBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ1QsbUZBQW1GO1lBQ25GLE9BQU8sQ0FBQyxLQUFLLENBQUMsMERBQTBELENBQUMsRUFBRSxDQUFDLENBQUM7UUFDakYsQ0FBQztJQUNMLENBQUM7SUFFRCx5RkFBeUY7SUFDekYsaURBQWlEO0lBQ2pELE1BQU0sSUFBQSwwQkFBZ0IsRUFBQztRQUNuQixVQUFVO1FBQ1YsT0FBTyxFQUFFLDBEQUEwRCxJQUFJLEtBQUs7Y0FDdEUsd0RBQXdEO0tBQ2pFLENBQUMsQ0FBQztJQUVILE9BQU8sRUFBRSxPQUFPLEVBQUUsV0FBVyxJQUFJLFVBQW1CLElBQUksU0FBa0IsRUFBRSxRQUFRLEVBQUUsQ0FBQztBQUMzRixDQUFDO0FBNUVELGtEQTRFQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEgsIGZvcm1hdFdlYmhvb2tGaWxlLCBwYXJzZVdlYmhvb2tGaWxlLCByZWRhY3RXZWJob29rVVJMLCBzZW5kVG9XZWJob29rVVJMIH0gZnJvbSBcIi4vZGlzY29yZFwiO1xuaW1wb3J0IHsgcmVhZFJlbW90ZUZpbGUsIHdyaXRlUmVtb3RlRmlsZSB9IGZyb20gXCIuLi9oZWxwZXJzL3JlbW90ZVNTSFwiO1xuXG5leHBvcnQgY29uc3QgUkVQTEFDRV9LRVlXT1JEID0gXCJyZXBsYWNlXCI7XG5jb25zdCBVTlBBUlNFQUJMRV9MQUJFTCA9IFwiKHVucGFyc2VhYmxlKVwiO1xuXG4vKiogUHV0cyBhIHdlYmhvb2sgb24gYSByZW1vdGUgaG9zdCwgcmVmdXNpbmcgdG8gY2xvYmJlciBhIGRpZmZlcmVudCBleGlzdGluZyBvbmUgdW5sZXNzIGFza2VkLlxuICAgIFJldHVybnMgd2hhdCBoYXBwZW5lZCwgc28gY2FsbGVycyBjYW4gZGVjaWRlIHdoZXRoZXIgdG8ga2VlcCBnb2luZy4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBlbnN1cmVSZW1vdGVXZWJob29rKGNvbmZpZzoge1xuICAgIGhvc3Q6IHN0cmluZztcbiAgICB3ZWJob29rVVJMOiBzdHJpbmc7XG4gICAgcmVwbGFjZTogYm9vbGVhbjtcbiAgICBmaWxlUGF0aD86IHN0cmluZztcbn0pIHtcbiAgICBsZXQgeyBob3N0LCB3ZWJob29rVVJMLCByZXBsYWNlIH0gPSBjb25maWc7XG4gICAgbGV0IGZpbGVQYXRoID0gY29uZmlnLmZpbGVQYXRoIHx8IERFRkFVTFRfV0VCSE9PS19GSUxFX1BBVEg7XG5cbiAgICBsZXQgcmVwbGFjZWRVUkwgPSBcIlwiO1xuICAgIGxldCBleGlzdGluZ0NvbnRlbnRzID0gYXdhaXQgcmVhZFJlbW90ZUZpbGUoeyBob3N0LCBmaWxlUGF0aCB9KTtcbiAgICBpZiAoZXhpc3RpbmdDb250ZW50cykge1xuICAgICAgICAvLyBBIGNvcnJ1cHQgZXhpc3RpbmcgZmlsZSBzdGlsbCBjb3VudHMgYXMgYSBjb25mbGljdCwgc28gXCJyZXBsYWNlXCIgY2FuIHJlcGFpciBpdCBpbnN0ZWFkXG4gICAgICAgIC8vIG9mIHRoZSBzZXR1cCBkZWFkIGVuZGluZyBvbiBhIHBhcnNlIGVycm9yLlxuICAgICAgICBsZXQgZXhpc3RpbmdVUkwgPSBVTlBBUlNFQUJMRV9MQUJFTDtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIGV4aXN0aW5nVVJMID0gcGFyc2VXZWJob29rRmlsZSh7IGNvbnRlbnRzOiBleGlzdGluZ0NvbnRlbnRzLCBzb3VyY2VOYW1lOiBgJHtob3N0fToke2ZpbGVQYXRofWAgfSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoYCR7aG9zdH06JHtmaWxlUGF0aH0gZXhpc3RzIGJ1dCBjb3VsZCBub3QgYmUgcGFyc2VkLiAke2V9YCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKGV4aXN0aW5nVVJMID09PSB3ZWJob29rVVJMKSB7XG4gICAgICAgICAgICByZXR1cm4geyBvdXRjb21lOiBcInVuY2hhbmdlZFwiIGFzIGNvbnN0LCBmaWxlUGF0aCB9O1xuICAgICAgICB9XG4gICAgICAgIGlmICghcmVwbGFjZSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAgICAgICAgIGBFeHBlY3RlZCBubyBjb25mbGljdGluZyB3ZWJob29rIG9uICR7aG9zdH0sIGJ1dCAke2ZpbGVQYXRofSBhbHJlYWR5IGhvbGRzIGEgZGlmZmVyZW50IG9uZS5cXG5gXG4gICAgICAgICAgICAgICAgKyBgRXhpc3Rpbmc6ICR7ZXhpc3RpbmdVUkx9XFxuYFxuICAgICAgICAgICAgICAgICsgYE5ldzogICAgICAke3dlYmhvb2tVUkx9XFxuYFxuICAgICAgICAgICAgICAgICsgYFBhc3MgXCIke1JFUExBQ0VfS0VZV09SRH1cIiB0byBvdmVyd3JpdGUgaXQuYFxuICAgICAgICAgICAgKTtcbiAgICAgICAgfVxuICAgICAgICByZXBsYWNlZFVSTCA9IGV4aXN0aW5nVVJMO1xuICAgIH1cblxuICAgIGF3YWl0IHdyaXRlUmVtb3RlRmlsZSh7XG4gICAgICAgIGhvc3QsXG4gICAgICAgIGZpbGVQYXRoLFxuICAgICAgICBjb250ZW50czogZm9ybWF0V2ViaG9va0ZpbGUod2ViaG9va1VSTCksXG4gICAgICAgIGZpbGVNb2RlOiBcIjYwMFwiLFxuICAgICAgICBkaXJlY3RvcnlNb2RlOiBcIjcwMFwiLFxuICAgIH0pO1xuXG4gICAgbGV0IHdyaXR0ZW5Db250ZW50cyA9IGF3YWl0IHJlYWRSZW1vdGVGaWxlKHsgaG9zdCwgZmlsZVBhdGggfSk7XG4gICAgaWYgKCF3cml0dGVuQ29udGVudHMpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBFeHBlY3RlZCAke2ZpbGVQYXRofSB0byBleGlzdCBvbiAke2hvc3R9IGFmdGVyIHdyaXRpbmcsIG5vIHN1Y2ggZmlsZSBleGlzdHNgKTtcbiAgICB9XG4gICAgbGV0IHdyaXR0ZW5VUkwgPSBwYXJzZVdlYmhvb2tGaWxlKHsgY29udGVudHM6IHdyaXR0ZW5Db250ZW50cywgc291cmNlTmFtZTogYCR7aG9zdH06JHtmaWxlUGF0aH1gIH0pO1xuICAgIGlmICh3cml0dGVuVVJMICE9PSB3ZWJob29rVVJMKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgJHtob3N0fToke2ZpbGVQYXRofSB0byBob2xkIHRoZSBuZXcgd2ViaG9vaywgd2FzICR7d3JpdHRlblVSTH1gKTtcbiAgICB9XG5cbiAgICBpZiAocmVwbGFjZWRVUkwgJiYgcmVwbGFjZWRVUkwgIT09IFVOUEFSU0VBQkxFX0xBQkVMKSB7XG4gICAgICAgIC8vIFRoZSBuZXcgd2ViaG9vayBpcyByZWRhY3RlZCwgc28gdGhpcyBjaGFubmVsIGNhbiBpZGVudGlmeSB0aGUgcmVwbGFjZW1lbnQgd2l0aG91dFxuICAgICAgICAvLyByZWNlaXZpbmcgYSB3ZWJob29rIGl0IGNvdWxkIHBvc3QgdG8uXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBhd2FpdCBzZW5kVG9XZWJob29rVVJMKHtcbiAgICAgICAgICAgICAgICB3ZWJob29rVVJMOiByZXBsYWNlZFVSTCxcbiAgICAgICAgICAgICAgICBtZXNzYWdlOiBgKipwb3J0c2VjdXJlKio6IHRoZSBEaXNjb3JkIHdlYmhvb2sgZm9yIFxcYCR7aG9zdH1cXGAgd2FzIHJlcGxhY2VkIHdpdGhgXG4gICAgICAgICAgICAgICAgICAgICsgYCBcXGAke3JlZGFjdFdlYmhvb2tVUkwod2ViaG9va1VSTCl9XFxgLmBcbiAgICAgICAgICAgICAgICAgICAgKyBgIFRoaXMgY2hhbm5lbCB3aWxsIHN0b3AgcmVjZWl2aW5nIG5vdGlmaWNhdGlvbnMgZm9yIHRoYXQgaG9zdC5gLFxuICAgICAgICAgICAgfSk7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIC8vIFRoZSByZXBsYWNlbWVudCBhbHJlYWR5IGhhcHBlbmVkLCBzbyBhIGRlYWQgb2xkIHdlYmhvb2sgbXVzdCBub3QgZmFpbCB0aGUgc2V0dXAuXG4gICAgICAgICAgICBjb25zb2xlLmVycm9yKGBDb3VsZCBub3Qgbm90aWZ5IHRoZSBvbGQgd2ViaG9vayB0aGF0IGl0IHdhcyByZXBsYWNlZC4gJHtlfWApO1xuICAgICAgICB9XG4gICAgfVxuXG4gICAgLy8gRmFpbGluZyBoZXJlIG1lYW5zIHRoZSBmaWxlIGlzIGluIHBsYWNlIGJ1dCB0aGUgd2ViaG9vayBpdHNlbGYgZG9lcyBub3Qgd29yaywgd2hpY2ggaXNcbiAgICAvLyBleGFjdGx5IHdoYXQgdGhlIG9wZXJhdG9yIG5lZWRzIHRvIGhlYXIgYWJvdXQuXG4gICAgYXdhaXQgc2VuZFRvV2ViaG9va1VSTCh7XG4gICAgICAgIHdlYmhvb2tVUkwsXG4gICAgICAgIG1lc3NhZ2U6IGAqKnBvcnRzZWN1cmUqKjogbm90aWZpY2F0aW9ucyBhcmUgbm93IGNvbmZpZ3VyZWQgZm9yIFxcYCR7aG9zdH1cXGAuYFxuICAgICAgICAgICAgKyBgIFRoaXMgY2hhbm5lbCB3aWxsIHJlY2VpdmUgaXRzIHNlY3VyaXR5IG5vdGlmaWNhdGlvbnMuYCxcbiAgICB9KTtcblxuICAgIHJldHVybiB7IG91dGNvbWU6IHJlcGxhY2VkVVJMICYmIFwicmVwbGFjZWRcIiBhcyBjb25zdCB8fCBcImNyZWF0ZWRcIiBhcyBjb25zdCwgZmlsZVBhdGggfTtcbn1cbiJdfQ==
78
+ /* _JS_SOURCE_HASH = "7383d5b5b9fee5c3deb4ea3e75d9c556e64aef84ab8d71e7683a02bc87c5070a"; */