sliftutils 1.7.148 → 1.7.150

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.
@@ -5,6 +5,8 @@ import { MANIFEST_NAME, normalizeContent, SIGN_NAMESPACE, SIGNATURE_NAME } from
5
5
  import { verifySSHSIG } from "../../signedFiles/sshsig";
6
6
  import { keyFingerprint } from "../authorizedKeys";
7
7
  import { revokeRepoPath } from "../revokeSource";
8
+ import { spawnPromise } from "../../helpers/spawn";
9
+ import { GIT_TIMEOUT } from "./paths";
8
10
 
9
11
  // A source that has never been signed reads as this, so losing a signature counts as a change of
10
12
  // signer rather than as something to wave through.
@@ -15,6 +17,25 @@ const SIGN_COMMAND = "yarn signfiles git";
15
17
  const REVOCATIONS_DIR = "revocations";
16
18
  const UNREVOKES_DIR = "unrevoked";
17
19
 
20
+ /** What the last commit holds for a file, whatever the working tree has done to it since.
21
+
22
+ Only ever consulted when what is on disk does not match the signature, so the normal case costs
23
+ no git at all. The content still has to match the manifest to be used, so a checkout that is
24
+ not a git repo, a path that was never committed, and content mangled on its way out of git all
25
+ end the same way: no match, and the file is left out. */
26
+ async function committedContents(repoPath: string, filePath: string) {
27
+ let result = await spawnPromise({
28
+ command: "git",
29
+ args: ["show", `HEAD:${filePath.split(path.sep).join("/")}`],
30
+ cwd: repoPath,
31
+ timeoutTime: GIT_TIMEOUT,
32
+ });
33
+ if (result.error || result.status !== 0) {
34
+ return undefined;
35
+ }
36
+ return Buffer.from(result.stdout, "utf8");
37
+ }
38
+
18
39
  // Every revocation this process has ever seen, keyed by revocation id. Once absorbed a revocation
19
40
  // never leaves, even if its file disappears from the revoke repo - the key that writes revocations
20
41
  // is on every server, so whoever stole one could otherwise erase the record that shut them out.
@@ -86,15 +107,6 @@ export async function readSignedRepo(config: { repoPath: string; sourceURL: stri
86
107
  files: Map<string, Buffer>;
87
108
  }> {
88
109
  let { repoPath, sourceURL } = config;
89
- let manifestBytes = await fs.readFile(path.join(repoPath, MANIFEST_NAME)).catch(() => undefined);
90
- let signatureBytes = await fs.readFile(path.join(repoPath, SIGNATURE_NAME)).catch(() => undefined);
91
- let manifestHash = manifestBytes && crypto.createHash("sha256").update(manifestBytes).digest("hex") || "";
92
- let signatureHash = signatureBytes && crypto.createHash("sha256").update(signatureBytes).digest("hex") || "";
93
-
94
- if (!manifestBytes && !signatureBytes) {
95
- console.log(`${repoPath} is not signed, ignoring all of its files`);
96
- return { signer: UNSIGNED, manifestHash, signatureHash, files: new Map() };
97
- }
98
110
 
99
111
  let broken = (detail: string) => new SignedRepoError(
100
112
  "corrupt",
@@ -103,72 +115,105 @@ export async function readSignedRepo(config: { repoPath: string; sourceURL: stri
103
115
  + ` this machine keeps using what it last accepted.\n\n${detail}`
104
116
  + `\n\nTo replace the signature, run in that repo:\n\`\`\`\n${SIGN_COMMAND}\n\`\`\``
105
117
  );
106
- if (!manifestBytes || !signatureBytes) {
107
- throw broken(`Only one of ${MANIFEST_NAME} and ${SIGNATURE_NAME} is present.`);
108
- }
109
118
 
110
- let signer: string;
111
- try {
112
- // The signature covers the normalised bytes, so a checkout that arrived with CRLF still
113
- // verifies rather than looking like tampering.
114
- signer = verifySSHSIG({
115
- signature: signatureBytes.toString("utf8"),
116
- message: normalizeContent(manifestBytes),
117
- namespace: SIGN_NAMESPACE,
118
- }).publicKey;
119
- } catch (e) {
120
- throw broken(`${e}`);
119
+ // The signature covers the normalised bytes, so a checkout that arrived with CRLF still
120
+ // verifies rather than looking like tampering.
121
+ let verifyPair = (manifest: Buffer | undefined, signature: Buffer | undefined) => {
122
+ if (!manifest || !signature) {
123
+ return undefined;
124
+ }
125
+ try {
126
+ return {
127
+ signer: verifySSHSIG({
128
+ signature: signature.toString("utf8"),
129
+ message: normalizeContent(manifest),
130
+ namespace: SIGN_NAMESPACE,
131
+ }).publicKey,
132
+ manifest,
133
+ manifestHash: crypto.createHash("sha256").update(manifest).digest("hex"),
134
+ signatureHash: crypto.createHash("sha256").update(signature).digest("hex"),
135
+ };
136
+ } catch (e) {
137
+ return undefined;
138
+ }
139
+ };
140
+
141
+ let diskManifest = await fs.readFile(path.join(repoPath, MANIFEST_NAME)).catch(() => undefined);
142
+ let diskSignature = await fs.readFile(path.join(repoPath, SIGNATURE_NAME)).catch(() => undefined);
143
+ // What the working tree holds is what we want, when it holds something that verifies.
144
+ let signed = verifyPair(diskManifest, diskSignature);
145
+ // Otherwise the last commit, which is the state somebody actually signed and pushed. An edit
146
+ // in progress, or one abandoned half way, then costs nothing: it is simply not what we read.
147
+ if (!signed) {
148
+ signed = verifyPair(
149
+ await committedContents(repoPath, MANIFEST_NAME),
150
+ await committedContents(repoPath, SIGNATURE_NAME)
151
+ );
152
+ if (signed) {
153
+ console.log(`${path.join(repoPath, MANIFEST_NAME)} does not verify as it is here, reading the committed one instead`);
154
+ }
121
155
  }
122
156
 
123
- // Every file on disk, other than git's own directory and the two signature files, which cannot
124
- // describe themselves.
125
- let listCheckoutFiles = async (prefix?: string): Promise<string[]> => {
126
- let found: string[] = [];
127
- for (let entry of await fs.readdir(path.join(repoPath, prefix || ""), { withFileTypes: true })) {
128
- let relativePath = prefix && `${prefix}/${entry.name}` || entry.name;
129
- if (entry.name === ".git") continue;
130
- if (!prefix && (entry.name === MANIFEST_NAME || entry.name === SIGNATURE_NAME)) continue;
131
- if (entry.isDirectory()) {
132
- found.push(...await listCheckoutFiles(relativePath));
133
- continue;
134
- }
135
- found.push(relativePath);
157
+ if (!signed) {
158
+ if (!diskManifest && !diskSignature) {
159
+ console.log(`${repoPath} is not signed, ignoring all of its files`);
160
+ return { signer: UNSIGNED, manifestHash: "", signatureHash: "", files: new Map() };
136
161
  }
137
- return found.sort();
138
- };
162
+ if (!diskManifest || !diskSignature) {
163
+ throw broken(`Only one of ${MANIFEST_NAME} and ${SIGNATURE_NAME} is present.`);
164
+ }
165
+ throw broken(`Neither the ${MANIFEST_NAME} here nor the committed one verifies.`);
166
+ }
167
+ let { signer, manifestHash, signatureHash } = signed;
168
+ let manifestBytes = signed.manifest;
139
169
 
170
+ // The manifest is the list of files, so it is the only thing we read. Whatever else is in the
171
+ // checkout is never opened and never mentioned - a working tree full of node_modules is not
172
+ // something to report on, it is simply not what was signed.
173
+ //
174
+ // A file counts only if its contents still match what the manifest says. One that does not is
175
+ // left out on its own: it is a file we hold no signature for, and it says nothing about the
176
+ // files that DO match. Only the signature over the manifest can discredit the whole repo.
140
177
  let manifest = JSON.parse(manifestBytes.toString("utf8"));
141
- let listed = new Map<string, { size: number; sha256: string }>(
142
- (manifest.files || []).map((file: { path: string; size: number; sha256: string }) => [file.path, file])
143
- );
178
+ let listed = (manifest.files || []) as { path: string; size: number; sha256: string }[];
179
+
180
+ let matchesManifest = (contents: Buffer | undefined, expected: { size: number; sha256: string }) => {
181
+ if (!contents) {
182
+ return undefined;
183
+ }
184
+ let normalized = normalizeContent(contents);
185
+ if (normalized.length !== expected.size) {
186
+ return undefined;
187
+ }
188
+ if (crypto.createHash("sha256").update(normalized).digest("hex") !== expected.sha256) {
189
+ return undefined;
190
+ }
191
+ return normalized;
192
+ };
144
193
 
145
- // A file counts only if the manifest names it AND its contents still match. Anything else is
146
- // left out, one file at a time: an unsigned file, a file edited after signing, and a file the
147
- // manifest names that is not here are all just files we have no signature for. None of them
148
- // says anything about the files that DO match, so none of them takes the rest of the repo down
149
- // with it - the signature over the manifest is the only thing that can do that.
150
194
  let files = new Map<string, Buffer>();
151
- for (let filePath of await listCheckoutFiles()) {
152
- let expected = listed.get(filePath);
153
- if (!expected) {
154
- console.log(`Ignoring ${path.join(repoPath, filePath)}, it is not in the signed manifest`);
155
- continue;
195
+ for (let expected of listed) {
196
+ let contents = matchesManifest(
197
+ await fs.readFile(path.join(repoPath, expected.path)).catch(() => undefined),
198
+ expected
199
+ );
200
+ // Same as for the manifest: what the working tree holds is preferred, and the committed
201
+ // file is what we fall back to. A file edited but not signed, or deleted by accident, then
202
+ // goes on working from the last state anybody signed.
203
+ if (!contents) {
204
+ contents = matchesManifest(await committedContents(repoPath, expected.path), expected);
205
+ if (contents) {
206
+ console.log(`${path.join(repoPath, expected.path)} does not match the manifest as it is here, using the committed one`);
207
+ }
156
208
  }
157
- let contents = normalizeContent(await fs.readFile(path.join(repoPath, filePath)));
158
- let hash = crypto.createHash("sha256").update(contents).digest("hex");
159
- if (contents.length !== expected.size || hash !== expected.sha256) {
209
+ if (!contents) {
160
210
  console.log(
161
- `Ignoring ${path.join(repoPath, filePath)}, it was changed after it was signed.`
162
- + ` Run ${SIGN_COMMAND} in that repo to sign it as it now is.`
211
+ `Ignoring ${path.join(repoPath, expected.path)}, neither it nor the committed version`
212
+ + ` matches the manifest. Run ${SIGN_COMMAND} in that repo to sign it as it now is.`
163
213
  );
164
214
  continue;
165
215
  }
166
- files.set(filePath, contents);
167
- }
168
- for (let filePath of listed.keys()) {
169
- if (!files.has(filePath)) {
170
- console.log(`${path.join(repoPath, filePath)} is in the signed manifest, but there is nothing here that matches it`);
171
- }
216
+ files.set(expected.path, contents);
172
217
  }
173
218
 
174
219
  // Absorb the revoke repo's files into the sticky map. Once seen a revocation never leaves it,
@@ -1,7 +1,7 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
3
  import { deriveRevokeKey, findRevokeKey, REVOKE_KEY_LABEL, revokeKeyPath, revokeRepoPath, revokeRepoURL } from "../revokeSource";
4
- import { findSourceKey, sourceKeyPath, sourceRepoPath } from "../sources";
4
+ import { describeMissingSourceKey, findSourceKey, sourceKeyPath, sourceRepoPath } from "../sources";
5
5
  import { cloneRepo, repoIsUsable, runGit, setGitRef } from "./git";
6
6
 
7
7
  /** Reading a repo, made to look like reading a directory. A caller asks for a file and gets its
@@ -42,9 +42,15 @@ export async function ensureRevokeKey(sourceURL: string) {
42
42
  }
43
43
  let sourceKey = await findSourceKey(sourceURL);
44
44
  if (!sourceKey) {
45
- throw new Error(`Expected a key for ${sourceURL} at ${sourceKeyPath(sourceURL)}, no such file exists`);
45
+ throw new Error(
46
+ `Expected the private key for ${sourceURL} to be one of these files, and it is neither:`
47
+ + ` ${await describeMissingSourceKey(sourceURL)}`
48
+ );
46
49
  }
47
- let derived = deriveRevokeKey(await fs.readFile(sourceKey, "utf8"));
50
+ let sourceKeyContents = await fs.readFile(sourceKey, "utf8").catch(e => {
51
+ throw new Error(`Expected to read the private key for ${sourceURL} from ${sourceKey}, ${e}`);
52
+ });
53
+ let derived = deriveRevokeKey(sourceKeyContents);
48
54
  let keyPath = revokeKeyPath(sourceURL);
49
55
  await fs.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
50
56
  await fs.writeFile(keyPath, derived.privateKeyFile, { mode: 0o600 });
@@ -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.findSourceKey = exports.findKey = exports.sourceRepoPath = exports.legacySourceKeyPath = exports.sourceKeyPath = exports.sourceName = exports.keysDir = exports.REPOS_DIR = exports.LEGACY_REPO_KEYS_DIR = exports.KEYS_DIR_NAME = void 0;
6
+ //exports.describeMissingSourceKey = exports.findSourceKey = exports.describeMissingKey = exports.findKey = exports.sourceRepoPath = exports.legacySourceKeyPath = exports.sourceKeyPath = exports.sourceName = exports.keysDir = exports.REPOS_DIR = exports.LEGACY_REPO_KEYS_DIR = exports.KEYS_DIR_NAME = void 0;
7
7
  const promises_1 = __importDefault(require("fs/promises"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const path_1 = __importDefault(require("path"));
@@ -38,31 +38,43 @@ function sourceRepoPath(repoURL) {
38
38
  return `${exports.REPOS_DIR}/${sourceName(repoURL)}`;
39
39
  }
40
40
  exports.sourceRepoPath = sourceRepoPath;
41
- async function pathExists(filePath) {
42
- try {
43
- await promises_1.default.access(filePath);
44
- return true;
45
- }
46
- catch (e) {
47
- return false;
48
- }
49
- }
50
41
  /** The key for a source, wherever it happens to be. The user folder is where they go now, and the
51
42
  old location is still read so a host that predates the move keeps working. */
43
+ /** A key has to be the file itself. Something else of that name - most often a directory somebody
44
+ made to put the key inside - is not a key, and answering with it means whoever reads it gets
45
+ "illegal operation on a directory" instead of being told the key is missing. */
46
+ async function keyFile(filePath) {
47
+ let stats = await promises_1.default.stat(filePath).catch(() => undefined);
48
+ return (stats === null || stats === void 0 ? void 0 : stats.isFile()) && filePath || "";
49
+ }
52
50
  async function findKey(config) {
53
- let { current, legacy } = config;
54
- if (await pathExists(current)) {
55
- return current;
56
- }
57
- if (await pathExists(legacy)) {
58
- return legacy;
59
- }
60
- return "";
51
+ return await keyFile(config.current) || await keyFile(config.legacy);
61
52
  }
62
53
  exports.findKey = findKey;
54
+ /** What is at each place the key could be, for whoever has to be told it was not found. "No such
55
+ file" is a lie when a directory of that name is sitting right there, and it is the one case
56
+ where saying so is the whole answer. */
57
+ async function describeMissingKey(config) {
58
+ let describe = async (filePath) => {
59
+ let stats = await promises_1.default.stat(filePath).catch(() => undefined);
60
+ if (!stats) {
61
+ return `${filePath} (nothing there)`;
62
+ }
63
+ if (stats.isDirectory()) {
64
+ return `${filePath} (a directory - the key has to BE this path, not a file inside it)`;
65
+ }
66
+ return `${filePath} (not a regular file)`;
67
+ };
68
+ return `${await describe(config.current)}, and ${await describe(config.legacy)}`;
69
+ }
70
+ exports.describeMissingKey = describeMissingKey;
63
71
  async function findSourceKey(repoURL) {
64
72
  return await findKey({ current: sourceKeyPath(repoURL), legacy: legacySourceKeyPath(repoURL) });
65
73
  }
66
74
  exports.findSourceKey = findSourceKey;
67
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic291cmNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNvdXJjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBQzdCLDRDQUFvQjtBQUNwQixnREFBd0I7QUFFeEIsZ0dBQWdHO0FBQ2hHLG1HQUFtRztBQUVuRyxrR0FBa0c7QUFDbEcsMENBQTBDO0FBQzdCLFFBQUEsYUFBYSxHQUFHLGlCQUFpQixDQUFDO0FBQy9DLGdHQUFnRztBQUNoRyx5QkFBeUI7QUFDWixRQUFBLG9CQUFvQixHQUFHLDJCQUEyQixDQUFDO0FBQ25ELFFBQUEsU0FBUyxHQUFHLDJDQUEyQyxDQUFDO0FBRXJFLFNBQWdCLE9BQU87SUFDbkIsT0FBTyxjQUFJLENBQUMsSUFBSSxDQUFDLFlBQUUsQ0FBQyxPQUFPLEVBQUUsRUFBRSxxQkFBYSxDQUFDLENBQUM7QUFDbEQsQ0FBQztBQUZELDBCQUVDO0FBRUQ7MkZBQzJGO0FBQzNGLFNBQWdCLFVBQVUsQ0FBQyxPQUFlO0lBQ3RDLE9BQU8sT0FBTyxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLEVBQUUsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDO0FBQ3hGLENBQUM7QUFGRCxnQ0FFQztBQUVELFNBQWdCLGFBQWEsQ0FBQyxPQUFlO0lBQ3pDLE9BQU8sY0FBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsRUFBRSxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztBQUNyRCxDQUFDO0FBRkQsc0NBRUM7QUFFRCxTQUFnQixtQkFBbUIsQ0FBQyxPQUFlO0lBQy9DLE9BQU8sR0FBRyw0QkFBb0IsSUFBSSxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztBQUM1RCxDQUFDO0FBRkQsa0RBRUM7QUFFRCxTQUFnQixjQUFjLENBQUMsT0FBZTtJQUMxQyxPQUFPLEdBQUcsaUJBQVMsSUFBSSxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztBQUNqRCxDQUFDO0FBRkQsd0NBRUM7QUFFRCxLQUFLLFVBQVUsVUFBVSxDQUFDLFFBQWdCO0lBQ3RDLElBQUksQ0FBQztRQUNELE1BQU0sa0JBQUUsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDMUIsT0FBTyxJQUFJLENBQUM7SUFDaEIsQ0FBQztJQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7UUFDVCxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0FBQ0wsQ0FBQztBQUVEO2lGQUNpRjtBQUMxRSxLQUFLLFVBQVUsT0FBTyxDQUFDLE1BQTJDO0lBQ3JFLElBQUksRUFBRSxPQUFPLEVBQUUsTUFBTSxFQUFFLEdBQUcsTUFBTSxDQUFDO0lBQ2pDLElBQUksTUFBTSxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztRQUM1QixPQUFPLE9BQU8sQ0FBQztJQUNuQixDQUFDO0lBQ0QsSUFBSSxNQUFNLFVBQVUsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQzNCLE9BQU8sTUFBTSxDQUFDO0lBQ2xCLENBQUM7SUFDRCxPQUFPLEVBQUUsQ0FBQztBQUNkLENBQUM7QUFURCwwQkFTQztBQUVNLEtBQUssVUFBVSxhQUFhLENBQUMsT0FBZTtJQUMvQyxPQUFPLE1BQU0sT0FBTyxDQUFDLEVBQUUsT0FBTyxFQUFFLGFBQWEsQ0FBQyxPQUFPLENBQUMsRUFBRSxNQUFNLEVBQUUsbUJBQW1CLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQ3BHLENBQUM7QUFGRCxzQ0FFQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnMvcHJvbWlzZXNcIjtcbmltcG9ydCBvcyBmcm9tIFwib3NcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbi8vIFBPUlRFRCBDT0RFOiBub3RoaW5nIGhlcmUgaXMgZHVwbGljYXRlZCBhbnkgbW9yZSwgdGhlIGRhZW1vbiBpbXBvcnRzIGl0IGRpcmVjdGx5LiBLZXB0IGluIG9uZVxuLy8gcGxhY2UgYmVjYXVzZSB0aGUgZGFlbW9uIGFuZCB0aGUgZGVwbG95IGhhdmUgdG8gYWdyZWUgb24gd2hlcmUgYSBzb3VyY2UncyBrZXkgYW5kIGNoZWNrb3V0IGxpdmUuXG5cbi8vIFRoZSB1c2VyJ3Mgb3duIGZvbGRlciwgc28gdGhpcyB3b3JrcyB0aGUgc2FtZSBvbiBhIG1hY2hpbmUgdGhhdCBoYXMgbm8gL2V0Yy4gVGhlIGRhZW1vbiBydW5zIGFzXG4vLyByb290LCBzbyBvbiBhIGhvc3QgdGhpcyBpcyByb290J3MgaG9tZS5cbmV4cG9ydCBjb25zdCBLRVlTX0RJUl9OQU1FID0gXCJhdXRob3JpemVkX2tleXNcIjtcbi8vIFdoZXJlIGtleXMgdXNlZCB0byBnby4gU3RpbGwgcmVhZCwgc28gYSBob3N0IHNldCB1cCBiZWZvcmUgdGhpcyBrZWVwcyB3b3JraW5nLCBidXQgbm90aGluZyBpc1xuLy8gd3JpdHRlbiBoZXJlIGFueSBtb3JlLlxuZXhwb3J0IGNvbnN0IExFR0FDWV9SRVBPX0tFWVNfRElSID0gXCIvZXRjL3BvcnRzZWN1cmUvcmVwby1rZXlzXCI7XG5leHBvcnQgY29uc3QgUkVQT1NfRElSID0gXCIvdmFyL2xpYi9wb3J0c2VjdXJlL2F1dGhvcml6ZWQta2V5cy1yZXBvc1wiO1xuXG5leHBvcnQgZnVuY3Rpb24ga2V5c0RpcigpIHtcbiAgICByZXR1cm4gcGF0aC5qb2luKG9zLmhvbWVkaXIoKSwgS0VZU19ESVJfTkFNRSk7XG59XG5cbi8qKiBBIHJlcG8gdXJsIHJlZHVjZWQgdG8gc29tZXRoaW5nIHVzYWJsZSBhcyBhIGZpbGUgbmFtZS4gRGVyaXZlZCByYXRoZXIgdGhhbiBjb25maWd1cmVkLCBzbyB0aGVcbiAgICBkYWVtb24gYW5kIHRoZSBkZXBsb3kgc2NyaXB0IGFsd2F5cyBhZ3JlZSBvbiB3aGVyZSBhIHNvdXJjZSdzIGtleSBhbmQgY2hlY2tvdXQgbGl2ZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzb3VyY2VOYW1lKHJlcG9VUkw6IHN0cmluZykge1xuICAgIHJldHVybiByZXBvVVJMLnJlcGxhY2UoL1teQS1aYS16MC05XSsvZywgXCItXCIpLnJlcGxhY2UoL14tK3wtKyQvZywgXCJcIikudG9Mb3dlckNhc2UoKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHNvdXJjZUtleVBhdGgocmVwb1VSTDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHBhdGguam9pbihrZXlzRGlyKCksIHNvdXJjZU5hbWUocmVwb1VSTCkpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gbGVnYWN5U291cmNlS2V5UGF0aChyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYCR7TEVHQUNZX1JFUE9fS0VZU19ESVJ9LyR7c291cmNlTmFtZShyZXBvVVJMKX1gO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gc291cmNlUmVwb1BhdGgocmVwb1VSTDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIGAke1JFUE9TX0RJUn0vJHtzb3VyY2VOYW1lKHJlcG9VUkwpfWA7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHBhdGhFeGlzdHMoZmlsZVBhdGg6IHN0cmluZykge1xuICAgIHRyeSB7XG4gICAgICAgIGF3YWl0IGZzLmFjY2VzcyhmaWxlUGF0aCk7XG4gICAgICAgIHJldHVybiB0cnVlO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbn1cblxuLyoqIFRoZSBrZXkgZm9yIGEgc291cmNlLCB3aGVyZXZlciBpdCBoYXBwZW5zIHRvIGJlLiBUaGUgdXNlciBmb2xkZXIgaXMgd2hlcmUgdGhleSBnbyBub3csIGFuZCB0aGVcbiAgICBvbGQgbG9jYXRpb24gaXMgc3RpbGwgcmVhZCBzbyBhIGhvc3QgdGhhdCBwcmVkYXRlcyB0aGUgbW92ZSBrZWVwcyB3b3JraW5nLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGZpbmRLZXkoY29uZmlnOiB7IGN1cnJlbnQ6IHN0cmluZzsgbGVnYWN5OiBzdHJpbmcgfSkge1xuICAgIGxldCB7IGN1cnJlbnQsIGxlZ2FjeSB9ID0gY29uZmlnO1xuICAgIGlmIChhd2FpdCBwYXRoRXhpc3RzKGN1cnJlbnQpKSB7XG4gICAgICAgIHJldHVybiBjdXJyZW50O1xuICAgIH1cbiAgICBpZiAoYXdhaXQgcGF0aEV4aXN0cyhsZWdhY3kpKSB7XG4gICAgICAgIHJldHVybiBsZWdhY3k7XG4gICAgfVxuICAgIHJldHVybiBcIlwiO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZmluZFNvdXJjZUtleShyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYXdhaXQgZmluZEtleSh7IGN1cnJlbnQ6IHNvdXJjZUtleVBhdGgocmVwb1VSTCksIGxlZ2FjeTogbGVnYWN5U291cmNlS2V5UGF0aChyZXBvVVJMKSB9KTtcbn1cbiJdfQ==
68
- /* _JS_SOURCE_HASH = "9f5e55c14512350a4741b91f05d443110c0b1041a09d3141b6c3cbc79612378f"; */
75
+ async function describeMissingSourceKey(repoURL) {
76
+ return await describeMissingKey({ current: sourceKeyPath(repoURL), legacy: legacySourceKeyPath(repoURL) });
77
+ }
78
+ exports.describeMissingSourceKey = describeMissingSourceKey;
79
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic291cmNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNvdXJjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsMkRBQTZCO0FBQzdCLDRDQUFvQjtBQUNwQixnREFBd0I7QUFFeEIsZ0dBQWdHO0FBQ2hHLG1HQUFtRztBQUVuRyxrR0FBa0c7QUFDbEcsMENBQTBDO0FBQzdCLFFBQUEsYUFBYSxHQUFHLGlCQUFpQixDQUFDO0FBQy9DLGdHQUFnRztBQUNoRyx5QkFBeUI7QUFDWixRQUFBLG9CQUFvQixHQUFHLDJCQUEyQixDQUFDO0FBQ25ELFFBQUEsU0FBUyxHQUFHLDJDQUEyQyxDQUFDO0FBRXJFLFNBQWdCLE9BQU87SUFDbkIsT0FBTyxjQUFJLENBQUMsSUFBSSxDQUFDLFlBQUUsQ0FBQyxPQUFPLEVBQUUsRUFBRSxxQkFBYSxDQUFDLENBQUM7QUFDbEQsQ0FBQztBQUZELDBCQUVDO0FBRUQ7MkZBQzJGO0FBQzNGLFNBQWdCLFVBQVUsQ0FBQyxPQUFlO0lBQ3RDLE9BQU8sT0FBTyxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsVUFBVSxFQUFFLEVBQUUsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDO0FBQ3hGLENBQUM7QUFGRCxnQ0FFQztBQUVELFNBQWdCLGFBQWEsQ0FBQyxPQUFlO0lBQ3pDLE9BQU8sY0FBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsRUFBRSxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztBQUNyRCxDQUFDO0FBRkQsc0NBRUM7QUFFRCxTQUFnQixtQkFBbUIsQ0FBQyxPQUFlO0lBQy9DLE9BQU8sR0FBRyw0QkFBb0IsSUFBSSxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztBQUM1RCxDQUFDO0FBRkQsa0RBRUM7QUFFRCxTQUFnQixjQUFjLENBQUMsT0FBZTtJQUMxQyxPQUFPLEdBQUcsaUJBQVMsSUFBSSxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztBQUNqRCxDQUFDO0FBRkQsd0NBRUM7QUFFRDtpRkFDaUY7QUFDakY7O21GQUVtRjtBQUNuRixLQUFLLFVBQVUsT0FBTyxDQUFDLFFBQWdCO0lBQ25DLElBQUksS0FBSyxHQUFHLE1BQU0sa0JBQUUsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQzNELE9BQU8sQ0FBQSxLQUFLLGFBQUwsS0FBSyx1QkFBTCxLQUFLLENBQUUsTUFBTSxFQUFFLEtBQUksUUFBUSxJQUFJLEVBQUUsQ0FBQztBQUM3QyxDQUFDO0FBRU0sS0FBSyxVQUFVLE9BQU8sQ0FBQyxNQUEyQztJQUNyRSxPQUFPLE1BQU0sT0FBTyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsSUFBSSxNQUFNLE9BQU8sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDekUsQ0FBQztBQUZELDBCQUVDO0FBRUQ7OzJDQUUyQztBQUNwQyxLQUFLLFVBQVUsa0JBQWtCLENBQUMsTUFBMkM7SUFDaEYsSUFBSSxRQUFRLEdBQUcsS0FBSyxFQUFFLFFBQWdCLEVBQUUsRUFBRTtRQUN0QyxJQUFJLEtBQUssR0FBRyxNQUFNLGtCQUFFLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUMzRCxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDVCxPQUFPLEdBQUcsUUFBUSxrQkFBa0IsQ0FBQztRQUN6QyxDQUFDO1FBQ0QsSUFBSSxLQUFLLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQztZQUN0QixPQUFPLEdBQUcsUUFBUSxvRUFBb0UsQ0FBQztRQUMzRixDQUFDO1FBQ0QsT0FBTyxHQUFHLFFBQVEsdUJBQXVCLENBQUM7SUFDOUMsQ0FBQyxDQUFDO0lBQ0YsT0FBTyxHQUFHLE1BQU0sUUFBUSxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsU0FBUyxNQUFNLFFBQVEsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztBQUNyRixDQUFDO0FBWkQsZ0RBWUM7QUFFTSxLQUFLLFVBQVUsYUFBYSxDQUFDLE9BQWU7SUFDL0MsT0FBTyxNQUFNLE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxhQUFhLENBQUMsT0FBTyxDQUFDLEVBQUUsTUFBTSxFQUFFLG1CQUFtQixDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FBQztBQUNwRyxDQUFDO0FBRkQsc0NBRUM7QUFFTSxLQUFLLFVBQVUsd0JBQXdCLENBQUMsT0FBZTtJQUMxRCxPQUFPLE1BQU0sa0JBQWtCLENBQUMsRUFBRSxPQUFPLEVBQUUsYUFBYSxDQUFDLE9BQU8sQ0FBQyxFQUFFLE1BQU0sRUFBRSxtQkFBbUIsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDL0csQ0FBQztBQUZELDREQUVDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZzIGZyb20gXCJmcy9wcm9taXNlc1wiO1xuaW1wb3J0IG9zIGZyb20gXCJvc1wiO1xuaW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIjtcblxuLy8gUE9SVEVEIENPREU6IG5vdGhpbmcgaGVyZSBpcyBkdXBsaWNhdGVkIGFueSBtb3JlLCB0aGUgZGFlbW9uIGltcG9ydHMgaXQgZGlyZWN0bHkuIEtlcHQgaW4gb25lXG4vLyBwbGFjZSBiZWNhdXNlIHRoZSBkYWVtb24gYW5kIHRoZSBkZXBsb3kgaGF2ZSB0byBhZ3JlZSBvbiB3aGVyZSBhIHNvdXJjZSdzIGtleSBhbmQgY2hlY2tvdXQgbGl2ZS5cblxuLy8gVGhlIHVzZXIncyBvd24gZm9sZGVyLCBzbyB0aGlzIHdvcmtzIHRoZSBzYW1lIG9uIGEgbWFjaGluZSB0aGF0IGhhcyBubyAvZXRjLiBUaGUgZGFlbW9uIHJ1bnMgYXNcbi8vIHJvb3QsIHNvIG9uIGEgaG9zdCB0aGlzIGlzIHJvb3QncyBob21lLlxuZXhwb3J0IGNvbnN0IEtFWVNfRElSX05BTUUgPSBcImF1dGhvcml6ZWRfa2V5c1wiO1xuLy8gV2hlcmUga2V5cyB1c2VkIHRvIGdvLiBTdGlsbCByZWFkLCBzbyBhIGhvc3Qgc2V0IHVwIGJlZm9yZSB0aGlzIGtlZXBzIHdvcmtpbmcsIGJ1dCBub3RoaW5nIGlzXG4vLyB3cml0dGVuIGhlcmUgYW55IG1vcmUuXG5leHBvcnQgY29uc3QgTEVHQUNZX1JFUE9fS0VZU19ESVIgPSBcIi9ldGMvcG9ydHNlY3VyZS9yZXBvLWtleXNcIjtcbmV4cG9ydCBjb25zdCBSRVBPU19ESVIgPSBcIi92YXIvbGliL3BvcnRzZWN1cmUvYXV0aG9yaXplZC1rZXlzLXJlcG9zXCI7XG5cbmV4cG9ydCBmdW5jdGlvbiBrZXlzRGlyKCkge1xuICAgIHJldHVybiBwYXRoLmpvaW4ob3MuaG9tZWRpcigpLCBLRVlTX0RJUl9OQU1FKTtcbn1cblxuLyoqIEEgcmVwbyB1cmwgcmVkdWNlZCB0byBzb21ldGhpbmcgdXNhYmxlIGFzIGEgZmlsZSBuYW1lLiBEZXJpdmVkIHJhdGhlciB0aGFuIGNvbmZpZ3VyZWQsIHNvIHRoZVxuICAgIGRhZW1vbiBhbmQgdGhlIGRlcGxveSBzY3JpcHQgYWx3YXlzIGFncmVlIG9uIHdoZXJlIGEgc291cmNlJ3Mga2V5IGFuZCBjaGVja291dCBsaXZlLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHNvdXJjZU5hbWUocmVwb1VSTDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHJlcG9VUkwucmVwbGFjZSgvW15BLVphLXowLTldKy9nLCBcIi1cIikucmVwbGFjZSgvXi0rfC0rJC9nLCBcIlwiKS50b0xvd2VyQ2FzZSgpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gc291cmNlS2V5UGF0aChyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gcGF0aC5qb2luKGtleXNEaXIoKSwgc291cmNlTmFtZShyZXBvVVJMKSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBsZWdhY3lTb3VyY2VLZXlQYXRoKHJlcG9VUkw6IHN0cmluZykge1xuICAgIHJldHVybiBgJHtMRUdBQ1lfUkVQT19LRVlTX0RJUn0vJHtzb3VyY2VOYW1lKHJlcG9VUkwpfWA7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBzb3VyY2VSZXBvUGF0aChyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYCR7UkVQT1NfRElSfS8ke3NvdXJjZU5hbWUocmVwb1VSTCl9YDtcbn1cblxuLyoqIFRoZSBrZXkgZm9yIGEgc291cmNlLCB3aGVyZXZlciBpdCBoYXBwZW5zIHRvIGJlLiBUaGUgdXNlciBmb2xkZXIgaXMgd2hlcmUgdGhleSBnbyBub3csIGFuZCB0aGVcbiAgICBvbGQgbG9jYXRpb24gaXMgc3RpbGwgcmVhZCBzbyBhIGhvc3QgdGhhdCBwcmVkYXRlcyB0aGUgbW92ZSBrZWVwcyB3b3JraW5nLiAqL1xuLyoqIEEga2V5IGhhcyB0byBiZSB0aGUgZmlsZSBpdHNlbGYuIFNvbWV0aGluZyBlbHNlIG9mIHRoYXQgbmFtZSAtIG1vc3Qgb2Z0ZW4gYSBkaXJlY3Rvcnkgc29tZWJvZHlcbiAgICBtYWRlIHRvIHB1dCB0aGUga2V5IGluc2lkZSAtIGlzIG5vdCBhIGtleSwgYW5kIGFuc3dlcmluZyB3aXRoIGl0IG1lYW5zIHdob2V2ZXIgcmVhZHMgaXQgZ2V0c1xuICAgIFwiaWxsZWdhbCBvcGVyYXRpb24gb24gYSBkaXJlY3RvcnlcIiBpbnN0ZWFkIG9mIGJlaW5nIHRvbGQgdGhlIGtleSBpcyBtaXNzaW5nLiAqL1xuYXN5bmMgZnVuY3Rpb24ga2V5RmlsZShmaWxlUGF0aDogc3RyaW5nKSB7XG4gICAgbGV0IHN0YXRzID0gYXdhaXQgZnMuc3RhdChmaWxlUGF0aCkuY2F0Y2goKCkgPT4gdW5kZWZpbmVkKTtcbiAgICByZXR1cm4gc3RhdHM/LmlzRmlsZSgpICYmIGZpbGVQYXRoIHx8IFwiXCI7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBmaW5kS2V5KGNvbmZpZzogeyBjdXJyZW50OiBzdHJpbmc7IGxlZ2FjeTogc3RyaW5nIH0pIHtcbiAgICByZXR1cm4gYXdhaXQga2V5RmlsZShjb25maWcuY3VycmVudCkgfHwgYXdhaXQga2V5RmlsZShjb25maWcubGVnYWN5KTtcbn1cblxuLyoqIFdoYXQgaXMgYXQgZWFjaCBwbGFjZSB0aGUga2V5IGNvdWxkIGJlLCBmb3Igd2hvZXZlciBoYXMgdG8gYmUgdG9sZCBpdCB3YXMgbm90IGZvdW5kLiBcIk5vIHN1Y2hcbiAgICBmaWxlXCIgaXMgYSBsaWUgd2hlbiBhIGRpcmVjdG9yeSBvZiB0aGF0IG5hbWUgaXMgc2l0dGluZyByaWdodCB0aGVyZSwgYW5kIGl0IGlzIHRoZSBvbmUgY2FzZVxuICAgIHdoZXJlIHNheWluZyBzbyBpcyB0aGUgd2hvbGUgYW5zd2VyLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRlc2NyaWJlTWlzc2luZ0tleShjb25maWc6IHsgY3VycmVudDogc3RyaW5nOyBsZWdhY3k6IHN0cmluZyB9KSB7XG4gICAgbGV0IGRlc2NyaWJlID0gYXN5bmMgKGZpbGVQYXRoOiBzdHJpbmcpID0+IHtcbiAgICAgICAgbGV0IHN0YXRzID0gYXdhaXQgZnMuc3RhdChmaWxlUGF0aCkuY2F0Y2goKCkgPT4gdW5kZWZpbmVkKTtcbiAgICAgICAgaWYgKCFzdGF0cykge1xuICAgICAgICAgICAgcmV0dXJuIGAke2ZpbGVQYXRofSAobm90aGluZyB0aGVyZSlgO1xuICAgICAgICB9XG4gICAgICAgIGlmIChzdGF0cy5pc0RpcmVjdG9yeSgpKSB7XG4gICAgICAgICAgICByZXR1cm4gYCR7ZmlsZVBhdGh9IChhIGRpcmVjdG9yeSAtIHRoZSBrZXkgaGFzIHRvIEJFIHRoaXMgcGF0aCwgbm90IGEgZmlsZSBpbnNpZGUgaXQpYDtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gYCR7ZmlsZVBhdGh9IChub3QgYSByZWd1bGFyIGZpbGUpYDtcbiAgICB9O1xuICAgIHJldHVybiBgJHthd2FpdCBkZXNjcmliZShjb25maWcuY3VycmVudCl9LCBhbmQgJHthd2FpdCBkZXNjcmliZShjb25maWcubGVnYWN5KX1gO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZmluZFNvdXJjZUtleShyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYXdhaXQgZmluZEtleSh7IGN1cnJlbnQ6IHNvdXJjZUtleVBhdGgocmVwb1VSTCksIGxlZ2FjeTogbGVnYWN5U291cmNlS2V5UGF0aChyZXBvVVJMKSB9KTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRlc2NyaWJlTWlzc2luZ1NvdXJjZUtleShyZXBvVVJMOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gYXdhaXQgZGVzY3JpYmVNaXNzaW5nS2V5KHsgY3VycmVudDogc291cmNlS2V5UGF0aChyZXBvVVJMKSwgbGVnYWN5OiBsZWdhY3lTb3VyY2VLZXlQYXRoKHJlcG9VUkwpIH0pO1xufVxuIl19
80
+ /* _JS_SOURCE_HASH = "251d19bea727e2ae11bfa013e5c5f73eb4dc697a4b54afcf3b6b5aee85e00d4b"; */
@@ -8,10 +8,16 @@ export declare function sourceName(repoURL: string): string;
8
8
  export declare function sourceKeyPath(repoURL: string): string;
9
9
  export declare function legacySourceKeyPath(repoURL: string): string;
10
10
  export declare function sourceRepoPath(repoURL: string): string;
11
- /** The key for a source, wherever it happens to be. The user folder is where they go now, and the
12
- old location is still read so a host that predates the move keeps working. */
13
11
  export declare function findKey(config: {
14
12
  current: string;
15
13
  legacy: string;
16
14
  }): Promise<string>;
15
+ /** What is at each place the key could be, for whoever has to be told it was not found. "No such
16
+ file" is a lie when a directory of that name is sitting right there, and it is the one case
17
+ where saying so is the whole answer. */
18
+ export declare function describeMissingKey(config: {
19
+ current: string;
20
+ legacy: string;
21
+ }): Promise<string>;
17
22
  export declare function findSourceKey(repoURL: string): Promise<string>;
23
+ export declare function describeMissingSourceKey(repoURL: string): Promise<string>;
@@ -35,28 +35,41 @@ export function sourceRepoPath(repoURL: string) {
35
35
  return `${REPOS_DIR}/${sourceName(repoURL)}`;
36
36
  }
37
37
 
38
- async function pathExists(filePath: string) {
39
- try {
40
- await fs.access(filePath);
41
- return true;
42
- } catch (e) {
43
- return false;
44
- }
45
- }
46
-
47
38
  /** The key for a source, wherever it happens to be. The user folder is where they go now, and the
48
39
  old location is still read so a host that predates the move keeps working. */
40
+ /** A key has to be the file itself. Something else of that name - most often a directory somebody
41
+ made to put the key inside - is not a key, and answering with it means whoever reads it gets
42
+ "illegal operation on a directory" instead of being told the key is missing. */
43
+ async function keyFile(filePath: string) {
44
+ let stats = await fs.stat(filePath).catch(() => undefined);
45
+ return stats?.isFile() && filePath || "";
46
+ }
47
+
49
48
  export async function findKey(config: { current: string; legacy: string }) {
50
- let { current, legacy } = config;
51
- if (await pathExists(current)) {
52
- return current;
53
- }
54
- if (await pathExists(legacy)) {
55
- return legacy;
56
- }
57
- return "";
49
+ return await keyFile(config.current) || await keyFile(config.legacy);
50
+ }
51
+
52
+ /** What is at each place the key could be, for whoever has to be told it was not found. "No such
53
+ file" is a lie when a directory of that name is sitting right there, and it is the one case
54
+ where saying so is the whole answer. */
55
+ export async function describeMissingKey(config: { current: string; legacy: string }) {
56
+ let describe = async (filePath: string) => {
57
+ let stats = await fs.stat(filePath).catch(() => undefined);
58
+ if (!stats) {
59
+ return `${filePath} (nothing there)`;
60
+ }
61
+ if (stats.isDirectory()) {
62
+ return `${filePath} (a directory - the key has to BE this path, not a file inside it)`;
63
+ }
64
+ return `${filePath} (not a regular file)`;
65
+ };
66
+ return `${await describe(config.current)}, and ${await describe(config.legacy)}`;
58
67
  }
59
68
 
60
69
  export async function findSourceKey(repoURL: string) {
61
70
  return await findKey({ current: sourceKeyPath(repoURL), legacy: legacySourceKeyPath(repoURL) });
62
71
  }
72
+
73
+ export async function describeMissingSourceKey(repoURL: string) {
74
+ return await describeMissingKey({ current: sourceKeyPath(repoURL), legacy: legacySourceKeyPath(repoURL) });
75
+ }
@@ -0,0 +1,98 @@
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
+ /*
3
+ NOTE: This whole verification process isn't secure if someone man-in-the-middles the IP. There is a way to make it more secure, but right now it is quite a bit of work. Here is the outline, though.
4
+
5
+ a) Use libsodium, do handshake ourself over unsecured websocket (or random self signed websocket), and use ChaCha20 for encryption so it's fast
6
+ - This lets us use ED25519 certificates.
7
+ b) Integrate with socket-function, So we can do everything with regular socket function calls. It's a lot nicer of an interface. We can even have it proxied to Python. As annoying as that is...
8
+ c) Use github pages to host the site, so we can also host the machine keys
9
+ - Every time they change, we need to redeploy, and... we don't get revocations without deploying. BUT... it's better than nothing.
10
+
11
+ NOTE: Just using real HTTPS certificates isn't really an alternative.
12
+ - They won't be bound to IPs
13
+ - Revoking them is a lot more difficult (and they won't be automatically revoked)
14
+ - Once you reach certain level of machines, you're going to run into problems with different certificates per machine
15
+ - You're still going to need a way to give servers access to either the certificates or a way to generate the certificates.
16
+
17
+ So doing it with our own code isn't about cost or difficulty. The reason we would want to implement it ourselves is because HTTPS wasn't made to create an internal network of trust. It's designed around web browsing, and it works well for that. However, for two-way authentication, there are significantly better systems.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
23
+ exports.verifyReply = exports.verifyRequest = exports.signRequest = void 0;
24
+ const js_sha256_1 = __importDefault(require("js-sha256"));
25
+ const misc_1 = require("socket-function/src/misc");
26
+ const certs_1 = require("../../misc/https/certs");
27
+ const machines_1 = require("./machines");
28
+ function dataHash(data) {
29
+ return js_sha256_1.default.sha256(JSON.stringify(data));
30
+ }
31
+ function signRequest(domain, config) {
32
+ let threadKeyCert = (0, certs_1.getThreadKeyCert)(domain);
33
+ let issuer = (0, certs_1.getIdentityCA)(domain);
34
+ let payload = {
35
+ targetId: config.targetId,
36
+ cert: threadKeyCert.cert.toString(),
37
+ certIssuer: issuer.cert.toString(),
38
+ data: config.data,
39
+ };
40
+ return { signature: (0, certs_1.sign)(threadKeyCert, payload), payload };
41
+ }
42
+ exports.signRequest = signRequest;
43
+ /** Returns the machineId that signed, the data, and the signed reply to send back - always
44
+ produced, so the caller's only job is to return it. Throws if the signature or certificate
45
+ chain do not check out, or if the request was addressed to someone we are not: ownIdentities
46
+ is every address this server answers to - there are always several, internal and external -
47
+ and the signed targetId must be one of them.
48
+
49
+ IMPORTANT! You still need to check the machine with isMachineAccepted if you want to know if
50
+ the machine is trusted. We just verify that they are who they say they are, not that who they
51
+ say they are is allowed. */
52
+ function verifyRequest(domain, signed, ownIdentities) {
53
+ let { signature, payload } = signed;
54
+ (0, certs_1.verify)(payload.cert, signature, payload);
55
+ (0, certs_1.validateCertificate)(domain, payload.cert, payload.certIssuer);
56
+ if (!ownIdentities.includes(payload.targetId)) {
57
+ throw new Error(`Request is for someone else. It is addressed to ${payload.targetId}, we are ${ownIdentities.join(", ")}`);
58
+ }
59
+ let machineId = (0, certs_1.getMachineId)((0, certs_1.getCommonName)(payload.certIssuer), domain);
60
+ let threadKeyCert = (0, certs_1.getThreadKeyCert)(domain);
61
+ let issuer = (0, certs_1.getIdentityCA)(domain);
62
+ let { data: requestData, ...requestRest } = payload;
63
+ let replyPayload = {
64
+ request: { ...requestRest, dataHash: dataHash(requestData) },
65
+ cert: threadKeyCert.cert.toString(),
66
+ certIssuer: issuer.cert.toString(),
67
+ };
68
+ return {
69
+ machineId,
70
+ data: payload.data,
71
+ reply: { signature: (0, certs_1.sign)(threadKeyCert, replyPayload), payload: replyPayload },
72
+ };
73
+ }
74
+ exports.verifyRequest = verifyRequest;
75
+ /** Returns the machineId that replied. Throws if the signature or certificate chain do not check
76
+ out, or if the reply does not answer the request we actually sent. On node the replying
77
+ machine must also be one of the trusted machines - the browser has no machine list, so there
78
+ that check is skipped. */
79
+ async function verifyReply(domain, request, reply) {
80
+ let { signature, payload } = reply;
81
+ (0, certs_1.verify)(payload.cert, signature, payload);
82
+ (0, certs_1.validateCertificate)(domain, payload.cert, payload.certIssuer);
83
+ let { data: requestData, ...requestRest } = request.payload;
84
+ let { dataHash: echoedHash, ...echoedRest } = payload.request;
85
+ if (echoedHash !== dataHash(requestData) || JSON.stringify(echoedRest) !== JSON.stringify(requestRest)) {
86
+ throw new Error(`Reply answers a different request than the one we sent`);
87
+ }
88
+ let machineId = (0, certs_1.getMachineId)((0, certs_1.getCommonName)(payload.certIssuer), domain);
89
+ if ((0, misc_1.isNode)()) {
90
+ if (!(await (0, machines_1.getTrustedMachines)()).some(machine => machine.machineId === machineId)) {
91
+ throw new Error(`Reply is signed by ${machineId}, which is not a trusted machine`);
92
+ }
93
+ }
94
+ return { machineId };
95
+ }
96
+ exports.verifyReply = verifyReply;
97
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWRlbnRpdHkyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiaWRlbnRpdHkyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7Ozs7Ozs7Ozs7Ozs7OztFQWdCRTs7Ozs7O0FBRUYsMERBQStCO0FBQy9CLG1EQUFrRDtBQUNsRCxrREFBeUk7QUFDekkseUNBQWdEO0FBMEJoRCxTQUFTLFFBQVEsQ0FBQyxJQUFhO0lBQzNCLE9BQU8sbUJBQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0FBQy9DLENBQUM7QUFFRCxTQUFnQixXQUFXLENBQUksTUFBYyxFQUFFLE1BQXFDO0lBQ2hGLElBQUksYUFBYSxHQUFHLElBQUEsd0JBQWdCLEVBQUMsTUFBTSxDQUFDLENBQUM7SUFDN0MsSUFBSSxNQUFNLEdBQUcsSUFBQSxxQkFBYSxFQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ25DLElBQUksT0FBTyxHQUFnQztRQUN2QyxRQUFRLEVBQUUsTUFBTSxDQUFDLFFBQVE7UUFDekIsSUFBSSxFQUFFLGFBQWEsQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFO1FBQ25DLFVBQVUsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRTtRQUNsQyxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUk7S0FDcEIsQ0FBQztJQUNGLE9BQU8sRUFBRSxTQUFTLEVBQUUsSUFBQSxZQUFJLEVBQUMsYUFBYSxFQUFFLE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxDQUFDO0FBQ2hFLENBQUM7QUFWRCxrQ0FVQztBQUVEOzs7Ozs7OzsrQkFRK0I7QUFDL0IsU0FBZ0IsYUFBYSxDQUFJLE1BQWMsRUFBRSxNQUF3QixFQUFFLGFBQXVCO0lBSzlGLElBQUksRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLEdBQUcsTUFBTSxDQUFDO0lBQ3BDLElBQUEsY0FBTSxFQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3pDLElBQUEsMkJBQW1CLEVBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQzlELElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO1FBQzVDLE1BQU0sSUFBSSxLQUFLLENBQUMsbURBQW1ELE9BQU8sQ0FBQyxRQUFRLFlBQVksYUFBYSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDL0gsQ0FBQztJQUNELElBQUksU0FBUyxHQUFHLElBQUEsb0JBQVksRUFBQyxJQUFBLHFCQUFhLEVBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQ3hFLElBQUksYUFBYSxHQUFHLElBQUEsd0JBQWdCLEVBQUMsTUFBTSxDQUFDLENBQUM7SUFDN0MsSUFBSSxNQUFNLEdBQUcsSUFBQSxxQkFBYSxFQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ25DLElBQUksRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLEdBQUcsV0FBVyxFQUFFLEdBQUcsT0FBTyxDQUFDO0lBQ3BELElBQUksWUFBWSxHQUEyQjtRQUN2QyxPQUFPLEVBQUUsRUFBRSxHQUFHLFdBQVcsRUFBRSxRQUFRLEVBQUUsUUFBUSxDQUFDLFdBQVcsQ0FBQyxFQUFFO1FBQzVELElBQUksRUFBRSxhQUFhLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRTtRQUNuQyxVQUFVLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUU7S0FDckMsQ0FBQztJQUNGLE9BQU87UUFDSCxTQUFTO1FBQ1QsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJO1FBQ2xCLEtBQUssRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFBLFlBQUksRUFBQyxhQUFhLEVBQUUsWUFBWSxDQUFDLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRTtLQUNqRixDQUFDO0FBQ04sQ0FBQztBQXpCRCxzQ0F5QkM7QUFFRDs7OzZCQUc2QjtBQUN0QixLQUFLLFVBQVUsV0FBVyxDQUFDLE1BQWMsRUFBRSxPQUErQixFQUFFLEtBQWtCO0lBQ2pHLElBQUksRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLEdBQUcsS0FBSyxDQUFDO0lBQ25DLElBQUEsY0FBTSxFQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQ3pDLElBQUEsMkJBQW1CLEVBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQzlELElBQUksRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLEdBQUcsV0FBVyxFQUFFLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQztJQUM1RCxJQUFJLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLFVBQVUsRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUM7SUFDOUQsSUFBSSxVQUFVLEtBQUssUUFBUSxDQUFDLFdBQVcsQ0FBQyxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLEtBQUssSUFBSSxDQUFDLFNBQVMsQ0FBQyxXQUFXLENBQUMsRUFBRSxDQUFDO1FBQ3JHLE1BQU0sSUFBSSxLQUFLLENBQUMsd0RBQXdELENBQUMsQ0FBQztJQUM5RSxDQUFDO0lBQ0QsSUFBSSxTQUFTLEdBQUcsSUFBQSxvQkFBWSxFQUFDLElBQUEscUJBQWEsRUFBQyxPQUFPLENBQUMsVUFBVSxDQUFDLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDeEUsSUFBSSxJQUFBLGFBQU0sR0FBRSxFQUFFLENBQUM7UUFDWCxJQUFJLENBQUMsQ0FBQyxNQUFNLElBQUEsNkJBQWtCLEdBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxTQUFTLEtBQUssU0FBUyxDQUFDLEVBQUUsQ0FBQztZQUNqRixNQUFNLElBQUksS0FBSyxDQUFDLHNCQUFzQixTQUFTLGtDQUFrQyxDQUFDLENBQUM7UUFDdkYsQ0FBQztJQUNMLENBQUM7SUFDRCxPQUFPLEVBQUUsU0FBUyxFQUFFLENBQUM7QUFDekIsQ0FBQztBQWhCRCxrQ0FnQkMiLCJzb3VyY2VzQ29udGVudCI6WyIvKlxuICAgIE5PVEU6IFRoaXMgd2hvbGUgdmVyaWZpY2F0aW9uIHByb2Nlc3MgaXNuJ3Qgc2VjdXJlIGlmIHNvbWVvbmUgbWFuLWluLXRoZS1taWRkbGVzIHRoZSBJUC4gVGhlcmUgaXMgYSB3YXkgdG8gbWFrZSBpdCBtb3JlIHNlY3VyZSwgYnV0IHJpZ2h0IG5vdyBpdCBpcyBxdWl0ZSBhIGJpdCBvZiB3b3JrLiBIZXJlIGlzIHRoZSBvdXRsaW5lLCB0aG91Z2guIFxuXG4gICAgYSkgVXNlIGxpYnNvZGl1bSwgZG8gaGFuZHNoYWtlIG91cnNlbGYgb3ZlciB1bnNlY3VyZWQgd2Vic29ja2V0IChvciByYW5kb20gc2VsZiBzaWduZWQgd2Vic29ja2V0KSwgYW5kIHVzZSBDaGFDaGEyMCBmb3IgZW5jcnlwdGlvbiBzbyBpdCdzIGZhc3RcbiAgICAgICAgLSBUaGlzIGxldHMgdXMgdXNlIEVEMjU1MTkgY2VydGlmaWNhdGVzLiBcbiAgICBiKSBJbnRlZ3JhdGUgd2l0aCBzb2NrZXQtZnVuY3Rpb24sIFNvIHdlIGNhbiBkbyBldmVyeXRoaW5nIHdpdGggcmVndWxhciBzb2NrZXQgZnVuY3Rpb24gY2FsbHMuIEl0J3MgYSBsb3QgbmljZXIgb2YgYW4gaW50ZXJmYWNlLiBXZSBjYW4gZXZlbiBoYXZlIGl0IHByb3hpZWQgdG8gUHl0aG9uLiBBcyBhbm5veWluZyBhcyB0aGF0IGlzLi4uXG4gICAgYykgVXNlIGdpdGh1YiBwYWdlcyB0byBob3N0IHRoZSBzaXRlLCBzbyB3ZSBjYW4gYWxzbyBob3N0IHRoZSBtYWNoaW5lIGtleXNcbiAgICAgICAgLSBFdmVyeSB0aW1lIHRoZXkgY2hhbmdlLCB3ZSBuZWVkIHRvIHJlZGVwbG95LCBhbmQuLi4gd2UgZG9uJ3QgZ2V0IHJldm9jYXRpb25zIHdpdGhvdXQgZGVwbG95aW5nLiBCVVQuLi4gaXQncyBiZXR0ZXIgdGhhbiBub3RoaW5nLlxuXG4gICAgTk9URTogSnVzdCB1c2luZyByZWFsIEhUVFBTIGNlcnRpZmljYXRlcyBpc24ndCByZWFsbHkgYW4gYWx0ZXJuYXRpdmUuXG4gICAgICAgIC0gVGhleSB3b24ndCBiZSBib3VuZCB0byBJUHNcbiAgICAgICAgLSBSZXZva2luZyB0aGVtIGlzIGEgbG90IG1vcmUgZGlmZmljdWx0IChhbmQgdGhleSB3b24ndCBiZSBhdXRvbWF0aWNhbGx5IHJldm9rZWQpXG4gICAgICAgIC0gT25jZSB5b3UgcmVhY2ggY2VydGFpbiBsZXZlbCBvZiBtYWNoaW5lcywgeW91J3JlIGdvaW5nIHRvIHJ1biBpbnRvIHByb2JsZW1zIHdpdGggZGlmZmVyZW50IGNlcnRpZmljYXRlcyBwZXIgbWFjaGluZVxuICAgICAgICAtIFlvdSdyZSBzdGlsbCBnb2luZyB0byBuZWVkIGEgd2F5IHRvIGdpdmUgc2VydmVycyBhY2Nlc3MgdG8gZWl0aGVyIHRoZSBjZXJ0aWZpY2F0ZXMgb3IgYSB3YXkgdG8gZ2VuZXJhdGUgdGhlIGNlcnRpZmljYXRlcy5cblxuICAgIFNvIGRvaW5nIGl0IHdpdGggb3VyIG93biBjb2RlIGlzbid0IGFib3V0IGNvc3Qgb3IgZGlmZmljdWx0eS4gVGhlIHJlYXNvbiB3ZSB3b3VsZCB3YW50IHRvIGltcGxlbWVudCBpdCBvdXJzZWx2ZXMgaXMgYmVjYXVzZSBIVFRQUyB3YXNuJ3QgbWFkZSB0byBjcmVhdGUgYW4gaW50ZXJuYWwgbmV0d29yayBvZiB0cnVzdC4gSXQncyBkZXNpZ25lZCBhcm91bmQgd2ViIGJyb3dzaW5nLCBhbmQgaXQgd29ya3Mgd2VsbCBmb3IgdGhhdC4gSG93ZXZlciwgZm9yIHR3by13YXkgYXV0aGVudGljYXRpb24sIHRoZXJlIGFyZSBzaWduaWZpY2FudGx5IGJldHRlciBzeXN0ZW1zLlxuKi9cblxuaW1wb3J0IHNoYTI1NiBmcm9tIFwianMtc2hhMjU2XCI7XG5pbXBvcnQgeyBpc05vZGUgfSBmcm9tIFwic29ja2V0LWZ1bmN0aW9uL3NyYy9taXNjXCI7XG5pbXBvcnQgeyBnZXRDb21tb25OYW1lLCBnZXRJZGVudGl0eUNBLCBnZXRNYWNoaW5lSWQsIGdldFRocmVhZEtleUNlcnQsIHNpZ24sIHZhbGlkYXRlQ2VydGlmaWNhdGUsIHZlcmlmeSB9IGZyb20gXCIuLi8uLi9taXNjL2h0dHBzL2NlcnRzXCI7XG5pbXBvcnQgeyBnZXRUcnVzdGVkTWFjaGluZXMgfSBmcm9tIFwiLi9tYWNoaW5lc1wiO1xuXG5leHBvcnQgdHlwZSBTaWduZWRSZXF1ZXN0PFQ+ID0ge1xuICAgIHNpZ25hdHVyZTogc3RyaW5nO1xuICAgIHBheWxvYWQ6IHtcbiAgICAgICAgLy8gVGhlIGlwIHdlIGFyZSB0YWxraW5nIHRvLlxuICAgICAgICB0YXJnZXRJZDogc3RyaW5nO1xuICAgICAgICAvLyBUaGUgdGhyZWFkIGNlcnRpZmljYXRlIHRoYXQgc2lnbmVkIHRoaXNcbiAgICAgICAgY2VydDogc3RyaW5nO1xuICAgICAgICAvLyBUaGUgbWFjaGluZSBDQSB0aGF0IGlzc3VlZCBjZXJ0IC0gdGhlIG1hY2hpbmVJZCBjb21lcyBmcm9tIGl0cyBjb21tb24gbmFtZVxuICAgICAgICBjZXJ0SXNzdWVyOiBzdHJpbmc7XG4gICAgICAgIGRhdGE6IFQ7XG4gICAgfTtcbn07XG5cbmV4cG9ydCB0eXBlIFNpZ25lZFJlcGx5ID0ge1xuICAgIHNpZ25hdHVyZTogc3RyaW5nO1xuICAgIHBheWxvYWQ6IHtcbiAgICAgICAgLy8gVGhlIHJlcXVlc3QgdGhpcyByZXBseSBhbnN3ZXJzLCB3aXRoIHRoZSBkYXRhIGNvbGxhcHNlZCB0byBpdHMgaGFzaCAtIHJlc2VuZGluZyB0aGVcbiAgICAgICAgLy8gZGF0YSB3b3VsZCBzYXkgbm90aGluZyB0aGUgaGFzaCBkb2VzIG5vdFxuICAgICAgICByZXF1ZXN0OiBPbWl0PFNpZ25lZFJlcXVlc3Q8dW5rbm93bj5bXCJwYXlsb2FkXCJdLCBcImRhdGFcIj4gJiB7IGRhdGFIYXNoOiBzdHJpbmcgfTtcbiAgICAgICAgY2VydDogc3RyaW5nO1xuICAgICAgICBjZXJ0SXNzdWVyOiBzdHJpbmc7XG4gICAgfTtcbn07XG5cbmZ1bmN0aW9uIGRhdGFIYXNoKGRhdGE6IHVua25vd24pIHtcbiAgICByZXR1cm4gc2hhMjU2LnNoYTI1NihKU09OLnN0cmluZ2lmeShkYXRhKSk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBzaWduUmVxdWVzdDxUPihkb21haW46IHN0cmluZywgY29uZmlnOiB7IHRhcmdldElkOiBzdHJpbmc7IGRhdGE6IFQgfSk6IFNpZ25lZFJlcXVlc3Q8VD4ge1xuICAgIGxldCB0aHJlYWRLZXlDZXJ0ID0gZ2V0VGhyZWFkS2V5Q2VydChkb21haW4pO1xuICAgIGxldCBpc3N1ZXIgPSBnZXRJZGVudGl0eUNBKGRvbWFpbik7XG4gICAgbGV0IHBheWxvYWQ6IFNpZ25lZFJlcXVlc3Q8VD5bXCJwYXlsb2FkXCJdID0ge1xuICAgICAgICB0YXJnZXRJZDogY29uZmlnLnRhcmdldElkLFxuICAgICAgICBjZXJ0OiB0aHJlYWRLZXlDZXJ0LmNlcnQudG9TdHJpbmcoKSxcbiAgICAgICAgY2VydElzc3VlcjogaXNzdWVyLmNlcnQudG9TdHJpbmcoKSxcbiAgICAgICAgZGF0YTogY29uZmlnLmRhdGEsXG4gICAgfTtcbiAgICByZXR1cm4geyBzaWduYXR1cmU6IHNpZ24odGhyZWFkS2V5Q2VydCwgcGF5bG9hZCksIHBheWxvYWQgfTtcbn1cblxuLyoqIFJldHVybnMgdGhlIG1hY2hpbmVJZCB0aGF0IHNpZ25lZCwgdGhlIGRhdGEsIGFuZCB0aGUgc2lnbmVkIHJlcGx5IHRvIHNlbmQgYmFjayAtIGFsd2F5c1xuICAgIHByb2R1Y2VkLCBzbyB0aGUgY2FsbGVyJ3Mgb25seSBqb2IgaXMgdG8gcmV0dXJuIGl0LiBUaHJvd3MgaWYgdGhlIHNpZ25hdHVyZSBvciBjZXJ0aWZpY2F0ZVxuICAgIGNoYWluIGRvIG5vdCBjaGVjayBvdXQsIG9yIGlmIHRoZSByZXF1ZXN0IHdhcyBhZGRyZXNzZWQgdG8gc29tZW9uZSB3ZSBhcmUgbm90OiBvd25JZGVudGl0aWVzXG4gICAgaXMgZXZlcnkgYWRkcmVzcyB0aGlzIHNlcnZlciBhbnN3ZXJzIHRvIC0gdGhlcmUgYXJlIGFsd2F5cyBzZXZlcmFsLCBpbnRlcm5hbCBhbmQgZXh0ZXJuYWwgLVxuICAgIGFuZCB0aGUgc2lnbmVkIHRhcmdldElkIG11c3QgYmUgb25lIG9mIHRoZW0uXG5cbiAgICBJTVBPUlRBTlQhIFlvdSBzdGlsbCBuZWVkIHRvIGNoZWNrIHRoZSBtYWNoaW5lIHdpdGggaXNNYWNoaW5lQWNjZXB0ZWQgaWYgeW91IHdhbnQgdG8ga25vdyBpZlxuICAgIHRoZSBtYWNoaW5lIGlzIHRydXN0ZWQuIFdlIGp1c3QgdmVyaWZ5IHRoYXQgdGhleSBhcmUgd2hvIHRoZXkgc2F5IHRoZXkgYXJlLCBub3QgdGhhdCB3aG8gdGhleVxuICAgIHNheSB0aGV5IGFyZSBpcyBhbGxvd2VkLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHZlcmlmeVJlcXVlc3Q8VD4oZG9tYWluOiBzdHJpbmcsIHNpZ25lZDogU2lnbmVkUmVxdWVzdDxUPiwgb3duSWRlbnRpdGllczogc3RyaW5nW10pOiB7XG4gICAgbWFjaGluZUlkOiBzdHJpbmc7XG4gICAgZGF0YTogVDtcbiAgICByZXBseTogU2lnbmVkUmVwbHk7XG59IHtcbiAgICBsZXQgeyBzaWduYXR1cmUsIHBheWxvYWQgfSA9IHNpZ25lZDtcbiAgICB2ZXJpZnkocGF5bG9hZC5jZXJ0LCBzaWduYXR1cmUsIHBheWxvYWQpO1xuICAgIHZhbGlkYXRlQ2VydGlmaWNhdGUoZG9tYWluLCBwYXlsb2FkLmNlcnQsIHBheWxvYWQuY2VydElzc3Vlcik7XG4gICAgaWYgKCFvd25JZGVudGl0aWVzLmluY2x1ZGVzKHBheWxvYWQudGFyZ2V0SWQpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgUmVxdWVzdCBpcyBmb3Igc29tZW9uZSBlbHNlLiBJdCBpcyBhZGRyZXNzZWQgdG8gJHtwYXlsb2FkLnRhcmdldElkfSwgd2UgYXJlICR7b3duSWRlbnRpdGllcy5qb2luKFwiLCBcIil9YCk7XG4gICAgfVxuICAgIGxldCBtYWNoaW5lSWQgPSBnZXRNYWNoaW5lSWQoZ2V0Q29tbW9uTmFtZShwYXlsb2FkLmNlcnRJc3N1ZXIpLCBkb21haW4pO1xuICAgIGxldCB0aHJlYWRLZXlDZXJ0ID0gZ2V0VGhyZWFkS2V5Q2VydChkb21haW4pO1xuICAgIGxldCBpc3N1ZXIgPSBnZXRJZGVudGl0eUNBKGRvbWFpbik7XG4gICAgbGV0IHsgZGF0YTogcmVxdWVzdERhdGEsIC4uLnJlcXVlc3RSZXN0IH0gPSBwYXlsb2FkO1xuICAgIGxldCByZXBseVBheWxvYWQ6IFNpZ25lZFJlcGx5W1wicGF5bG9hZFwiXSA9IHtcbiAgICAgICAgcmVxdWVzdDogeyAuLi5yZXF1ZXN0UmVzdCwgZGF0YUhhc2g6IGRhdGFIYXNoKHJlcXVlc3REYXRhKSB9LFxuICAgICAgICBjZXJ0OiB0aHJlYWRLZXlDZXJ0LmNlcnQudG9TdHJpbmcoKSxcbiAgICAgICAgY2VydElzc3VlcjogaXNzdWVyLmNlcnQudG9TdHJpbmcoKSxcbiAgICB9O1xuICAgIHJldHVybiB7XG4gICAgICAgIG1hY2hpbmVJZCxcbiAgICAgICAgZGF0YTogcGF5bG9hZC5kYXRhLFxuICAgICAgICByZXBseTogeyBzaWduYXR1cmU6IHNpZ24odGhyZWFkS2V5Q2VydCwgcmVwbHlQYXlsb2FkKSwgcGF5bG9hZDogcmVwbHlQYXlsb2FkIH0sXG4gICAgfTtcbn1cblxuLyoqIFJldHVybnMgdGhlIG1hY2hpbmVJZCB0aGF0IHJlcGxpZWQuIFRocm93cyBpZiB0aGUgc2lnbmF0dXJlIG9yIGNlcnRpZmljYXRlIGNoYWluIGRvIG5vdCBjaGVja1xuICAgIG91dCwgb3IgaWYgdGhlIHJlcGx5IGRvZXMgbm90IGFuc3dlciB0aGUgcmVxdWVzdCB3ZSBhY3R1YWxseSBzZW50LiBPbiBub2RlIHRoZSByZXBseWluZ1xuICAgIG1hY2hpbmUgbXVzdCBhbHNvIGJlIG9uZSBvZiB0aGUgdHJ1c3RlZCBtYWNoaW5lcyAtIHRoZSBicm93c2VyIGhhcyBubyBtYWNoaW5lIGxpc3QsIHNvIHRoZXJlXG4gICAgdGhhdCBjaGVjayBpcyBza2lwcGVkLiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHZlcmlmeVJlcGx5KGRvbWFpbjogc3RyaW5nLCByZXF1ZXN0OiBTaWduZWRSZXF1ZXN0PHVua25vd24+LCByZXBseTogU2lnbmVkUmVwbHkpOiBQcm9taXNlPHsgbWFjaGluZUlkOiBzdHJpbmcgfT4ge1xuICAgIGxldCB7IHNpZ25hdHVyZSwgcGF5bG9hZCB9ID0gcmVwbHk7XG4gICAgdmVyaWZ5KHBheWxvYWQuY2VydCwgc2lnbmF0dXJlLCBwYXlsb2FkKTtcbiAgICB2YWxpZGF0ZUNlcnRpZmljYXRlKGRvbWFpbiwgcGF5bG9hZC5jZXJ0LCBwYXlsb2FkLmNlcnRJc3N1ZXIpO1xuICAgIGxldCB7IGRhdGE6IHJlcXVlc3REYXRhLCAuLi5yZXF1ZXN0UmVzdCB9ID0gcmVxdWVzdC5wYXlsb2FkO1xuICAgIGxldCB7IGRhdGFIYXNoOiBlY2hvZWRIYXNoLCAuLi5lY2hvZWRSZXN0IH0gPSBwYXlsb2FkLnJlcXVlc3Q7XG4gICAgaWYgKGVjaG9lZEhhc2ggIT09IGRhdGFIYXNoKHJlcXVlc3REYXRhKSB8fCBKU09OLnN0cmluZ2lmeShlY2hvZWRSZXN0KSAhPT0gSlNPTi5zdHJpbmdpZnkocmVxdWVzdFJlc3QpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgUmVwbHkgYW5zd2VycyBhIGRpZmZlcmVudCByZXF1ZXN0IHRoYW4gdGhlIG9uZSB3ZSBzZW50YCk7XG4gICAgfVxuICAgIGxldCBtYWNoaW5lSWQgPSBnZXRNYWNoaW5lSWQoZ2V0Q29tbW9uTmFtZShwYXlsb2FkLmNlcnRJc3N1ZXIpLCBkb21haW4pO1xuICAgIGlmIChpc05vZGUoKSkge1xuICAgICAgICBpZiAoIShhd2FpdCBnZXRUcnVzdGVkTWFjaGluZXMoKSkuc29tZShtYWNoaW5lID0+IG1hY2hpbmUubWFjaGluZUlkID09PSBtYWNoaW5lSWQpKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFJlcGx5IGlzIHNpZ25lZCBieSAke21hY2hpbmVJZH0sIHdoaWNoIGlzIG5vdCBhIHRydXN0ZWQgbWFjaGluZWApO1xuICAgICAgICB9XG4gICAgfVxuICAgIHJldHVybiB7IG1hY2hpbmVJZCB9O1xufVxuIl19
98
+ /* _JS_SOURCE_HASH = "231e08f06ef495826ab8e263ad2ae391434703a0403260056fc1824b8ad90ec5"; */