@devtion/actions 0.0.0-270e9e0 → 0.0.0-2ed8e18
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.
- package/dist/index.mjs +103 -110
- package/dist/index.node.js +102 -109
- package/dist/types/src/helpers/constants.d.ts +6 -0
- package/dist/types/src/helpers/constants.d.ts.map +1 -1
- package/dist/types/src/helpers/security.d.ts +2 -2
- package/dist/types/src/helpers/security.d.ts.map +1 -1
- package/dist/types/src/helpers/utils.d.ts.map +1 -1
- package/dist/types/src/helpers/vm.d.ts.map +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/package.json +2 -6
- package/src/helpers/constants.ts +7 -1
- package/src/helpers/functions.ts +1 -1
- package/src/helpers/security.ts +28 -50
- package/src/helpers/services.ts +1 -1
- package/src/helpers/utils.ts +77 -83
- package/src/helpers/vm.ts +9 -3
- package/src/index.ts +2 -2
- package/src/types/index.ts +9 -5
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module @
|
|
3
|
-
* @version 1.0.
|
|
2
|
+
* @module @devtion/actions
|
|
3
|
+
* @version 1.0.9
|
|
4
4
|
* @file A set of actions and helpers for CLI commands
|
|
5
5
|
* @copyright Ethereum Foundation 2022
|
|
6
6
|
* @license MIT
|
|
@@ -17,8 +17,7 @@ import crypto from 'crypto';
|
|
|
17
17
|
import blake from 'blakejs';
|
|
18
18
|
import { utils } from 'ffjavascript';
|
|
19
19
|
import winston from 'winston';
|
|
20
|
-
import {
|
|
21
|
-
import { pipeline, Readable } from 'stream';
|
|
20
|
+
import { pipeline } from 'stream';
|
|
22
21
|
import { promisify } from 'util';
|
|
23
22
|
import { initializeApp } from 'firebase/app';
|
|
24
23
|
import { signInWithCredential, initializeAuth, getAuth } from 'firebase/auth';
|
|
@@ -244,6 +243,12 @@ const commonTerms = {
|
|
|
244
243
|
verificationStartedAt: "verificationStartedAt"
|
|
245
244
|
}
|
|
246
245
|
},
|
|
246
|
+
avatars: {
|
|
247
|
+
name: "avatars",
|
|
248
|
+
fields: {
|
|
249
|
+
avatarUrl: "avatarUrl"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
247
252
|
ceremonies: {
|
|
248
253
|
name: "ceremonies",
|
|
249
254
|
fields: {
|
|
@@ -334,7 +339,7 @@ const commonTerms = {
|
|
|
334
339
|
finalizeCircuit: "finalizeCircuit",
|
|
335
340
|
finalizeCeremony: "finalizeCeremony",
|
|
336
341
|
downloadCircuitArtifacts: "downloadCircuitArtifacts",
|
|
337
|
-
transferObject: "transferObject"
|
|
342
|
+
transferObject: "transferObject"
|
|
338
343
|
}
|
|
339
344
|
};
|
|
340
345
|
|
|
@@ -1052,7 +1057,8 @@ const parseCeremonyFile = async (path, cleanup = false) => {
|
|
|
1052
1057
|
// read the data
|
|
1053
1058
|
const data = JSON.parse(fs.readFileSync(path).toString());
|
|
1054
1059
|
// verify that the data is correct
|
|
1055
|
-
if (data[
|
|
1060
|
+
if (data["timeoutMechanismType"] !== "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */ &&
|
|
1061
|
+
data["timeoutMechanismType"] !== "FIXED" /* CeremonyTimeoutType.FIXED */)
|
|
1056
1062
|
throw new Error("Invalid timeout type. Please choose between DYNAMIC and FIXED.");
|
|
1057
1063
|
// validate that we have at least 1 circuit input data
|
|
1058
1064
|
if (!data.circuits || data.circuits.length === 0)
|
|
@@ -1085,42 +1091,26 @@ const parseCeremonyFile = async (path, cleanup = false) => {
|
|
|
1085
1091
|
circuitArtifacts.push({
|
|
1086
1092
|
artifacts: artifacts
|
|
1087
1093
|
});
|
|
1088
|
-
const r1csPath = artifacts.r1csStoragePath;
|
|
1089
|
-
const wasmPath = artifacts.wasmStoragePath;
|
|
1090
1094
|
// where we storing the r1cs downloaded
|
|
1091
1095
|
const localR1csPath = `./${circuitData.name}.r1cs`;
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1094
|
-
// just the correct region
|
|
1095
|
-
const s3 = new S3Client({ region: artifacts.region });
|
|
1096
|
-
try {
|
|
1097
|
-
await s3.send(new HeadObjectCommand({
|
|
1098
|
-
Bucket: artifacts.bucket,
|
|
1099
|
-
Key: r1csPath
|
|
1100
|
-
}));
|
|
1101
|
-
}
|
|
1102
|
-
catch (error) {
|
|
1103
|
-
throw new Error(`The r1cs file (${r1csPath}) seems to not exist. Please ensure this is correct and that the object is publicly available.`);
|
|
1104
|
-
}
|
|
1105
|
-
try {
|
|
1106
|
-
await s3.send(new HeadObjectCommand({
|
|
1107
|
-
Bucket: artifacts.bucket,
|
|
1108
|
-
Key: wasmPath
|
|
1109
|
-
}));
|
|
1110
|
-
}
|
|
1111
|
-
catch (error) {
|
|
1112
|
-
throw new Error(`The wasm file (${wasmPath}) seems to not exist. Please ensure this is correct and that the object is publicly available.`);
|
|
1113
|
-
}
|
|
1096
|
+
// where we storing the wasm downloaded
|
|
1097
|
+
const localWasmPath = `./${circuitData.name}.wasm`;
|
|
1114
1098
|
// download the r1cs to extract the metadata
|
|
1115
|
-
const command = new GetObjectCommand({ Bucket: artifacts.bucket, Key: artifacts.r1csStoragePath });
|
|
1116
|
-
const response = await s3.send(command);
|
|
1117
1099
|
const streamPipeline = promisify(pipeline);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1100
|
+
// Make the call.
|
|
1101
|
+
const responseR1CS = await fetch(artifacts.r1csStoragePath);
|
|
1102
|
+
// Handle errors.
|
|
1103
|
+
if (!responseR1CS.ok && responseR1CS.status !== 200)
|
|
1104
|
+
throw new Error(`There was an error while trying to download the r1cs file for circuit ${circuitData.name}. Please check that the file has the correct permissions (public) set.`);
|
|
1105
|
+
await streamPipeline(responseR1CS.body, createWriteStream(localR1csPath));
|
|
1106
|
+
// Write the file locally
|
|
1122
1107
|
// extract the metadata from the r1cs
|
|
1123
1108
|
const metadata = getR1CSInfo(localR1csPath);
|
|
1109
|
+
// download wasm too to ensure it's available
|
|
1110
|
+
const responseWASM = await fetch(artifacts.wasmStoragePath);
|
|
1111
|
+
if (!responseWASM.ok && responseWASM.status !== 200)
|
|
1112
|
+
throw new Error(`There was an error while trying to download the WASM file for circuit ${circuitData.name}. Please check that the file has the correct permissions (public) set.`);
|
|
1113
|
+
await streamPipeline(responseWASM.body, createWriteStream(localWasmPath));
|
|
1124
1114
|
// validate that the circuit hash and template links are valid
|
|
1125
1115
|
const template = circuitData.template;
|
|
1126
1116
|
const URLMatch = template.source.match(urlPattern);
|
|
@@ -1138,7 +1128,7 @@ const parseCeremonyFile = async (path, cleanup = false) => {
|
|
|
1138
1128
|
const wasmCompleteFilename = `${circuitData.name}.wasm`;
|
|
1139
1129
|
const smallestPowersOfTauCompleteFilenameForCircuit = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`;
|
|
1140
1130
|
const firstZkeyCompleteFilename = `${circuitPrefix}_${genesisZkeyIndex}.zkey`;
|
|
1141
|
-
// storage paths
|
|
1131
|
+
// storage paths
|
|
1142
1132
|
const r1csStorageFilePath = getR1csStorageFilePath(circuitPrefix, r1csCompleteFilename);
|
|
1143
1133
|
const wasmStorageFilePath = getWasmStorageFilePath(circuitPrefix, wasmCompleteFilename);
|
|
1144
1134
|
const potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit);
|
|
@@ -1154,7 +1144,7 @@ const parseCeremonyFile = async (path, cleanup = false) => {
|
|
|
1154
1144
|
initialZkeyStoragePath: zkeyStorageFilePath,
|
|
1155
1145
|
r1csBlake2bHash: r1csBlake2bHash
|
|
1156
1146
|
};
|
|
1157
|
-
// validate that the compiler hash is a valid hash
|
|
1147
|
+
// validate that the compiler hash is a valid hash
|
|
1158
1148
|
const compiler = circuitData.compiler;
|
|
1159
1149
|
const compilerHashMatch = compiler.commitHash.match(commitHashPattern);
|
|
1160
1150
|
if (!compilerHashMatch || compilerHashMatch.length === 0 || compilerHashMatch.length > 1)
|
|
@@ -1168,39 +1158,58 @@ const parseCeremonyFile = async (path, cleanup = false) => {
|
|
|
1168
1158
|
// check that the timeout is provided for the correct configuration
|
|
1169
1159
|
let dynamicThreshold;
|
|
1170
1160
|
let fixedTimeWindow;
|
|
1161
|
+
let circuit = {};
|
|
1171
1162
|
if (data.timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */) {
|
|
1172
1163
|
if (circuitData.dynamicThreshold <= 0)
|
|
1173
1164
|
throw new Error("The dynamic threshold should be > 0.");
|
|
1174
1165
|
dynamicThreshold = circuitData.dynamicThreshold;
|
|
1166
|
+
// the Circuit data for the ceremony setup
|
|
1167
|
+
circuit = {
|
|
1168
|
+
name: circuitData.name,
|
|
1169
|
+
description: circuitData.description,
|
|
1170
|
+
prefix: circuitPrefix,
|
|
1171
|
+
sequencePosition: i + 1,
|
|
1172
|
+
metadata: metadata,
|
|
1173
|
+
files: files,
|
|
1174
|
+
template: template,
|
|
1175
|
+
compiler: compiler,
|
|
1176
|
+
verification: verification,
|
|
1177
|
+
dynamicThreshold: dynamicThreshold,
|
|
1178
|
+
avgTimings: {
|
|
1179
|
+
contributionComputation: 0,
|
|
1180
|
+
fullContribution: 0,
|
|
1181
|
+
verifyCloudFunction: 0
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1175
1184
|
}
|
|
1176
1185
|
if (data.timeoutMechanismType === "FIXED" /* CeremonyTimeoutType.FIXED */) {
|
|
1177
1186
|
if (circuitData.fixedTimeWindow <= 0)
|
|
1178
1187
|
throw new Error("The fixed time window threshold should be > 0.");
|
|
1179
1188
|
fixedTimeWindow = circuitData.fixedTimeWindow;
|
|
1189
|
+
// the Circuit data for the ceremony setup
|
|
1190
|
+
circuit = {
|
|
1191
|
+
name: circuitData.name,
|
|
1192
|
+
description: circuitData.description,
|
|
1193
|
+
prefix: circuitPrefix,
|
|
1194
|
+
sequencePosition: i + 1,
|
|
1195
|
+
metadata: metadata,
|
|
1196
|
+
files: files,
|
|
1197
|
+
template: template,
|
|
1198
|
+
compiler: compiler,
|
|
1199
|
+
verification: verification,
|
|
1200
|
+
fixedTimeWindow: fixedTimeWindow,
|
|
1201
|
+
avgTimings: {
|
|
1202
|
+
contributionComputation: 0,
|
|
1203
|
+
fullContribution: 0,
|
|
1204
|
+
verifyCloudFunction: 0
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1180
1207
|
}
|
|
1181
|
-
// the Circuit data for the ceremony setup
|
|
1182
|
-
const circuit = {
|
|
1183
|
-
name: circuitData.name,
|
|
1184
|
-
description: circuitData.description,
|
|
1185
|
-
prefix: circuitPrefix,
|
|
1186
|
-
sequencePosition: i + 1,
|
|
1187
|
-
metadata: metadata,
|
|
1188
|
-
files: files,
|
|
1189
|
-
template: template,
|
|
1190
|
-
compiler: compiler,
|
|
1191
|
-
verification: verification,
|
|
1192
|
-
fixedTimeWindow: fixedTimeWindow,
|
|
1193
|
-
// dynamicThreshold: dynamicThreshold,
|
|
1194
|
-
avgTimings: {
|
|
1195
|
-
contributionComputation: 0,
|
|
1196
|
-
fullContribution: 0,
|
|
1197
|
-
verifyCloudFunction: 0
|
|
1198
|
-
},
|
|
1199
|
-
};
|
|
1200
1208
|
circuits.push(circuit);
|
|
1201
|
-
// remove the local r1cs
|
|
1209
|
+
// remove the local r1cs and wasm downloads (if used for verifying the config only vs setup)
|
|
1202
1210
|
if (cleanup)
|
|
1203
1211
|
fs.unlinkSync(localR1csPath);
|
|
1212
|
+
fs.unlinkSync(localWasmPath);
|
|
1204
1213
|
}
|
|
1205
1214
|
const setupData = {
|
|
1206
1215
|
ceremonyInputData: {
|
|
@@ -1354,7 +1363,9 @@ const getContributionsValidityForContributor = async (firestoreDatabase, circuit
|
|
|
1354
1363
|
* @param isFinalizing <boolean> - true when the coordinator is finalizing the ceremony, otherwise false.
|
|
1355
1364
|
* @returns <string> - the public attestation preamble.
|
|
1356
1365
|
*/
|
|
1357
|
-
const getPublicAttestationPreambleForContributor = (contributorIdentifier, ceremonyName, isFinalizing) => `Hey, I'm ${contributorIdentifier} and I have ${isFinalizing ? "finalized" : "contributed to"} the ${ceremonyName}
|
|
1366
|
+
const getPublicAttestationPreambleForContributor = (contributorIdentifier, ceremonyName, isFinalizing) => `Hey, I'm ${contributorIdentifier} and I have ${isFinalizing ? "finalized" : "contributed to"} the ${ceremonyName}${ceremonyName.toLowerCase().includes("trusted setup") || ceremonyName.toLowerCase().includes("ceremony")
|
|
1367
|
+
? "."
|
|
1368
|
+
: " MPC Phase2 Trusted Setup ceremony."}\nThe following are my contribution signatures:`;
|
|
1358
1369
|
/**
|
|
1359
1370
|
* Check and prepare public attestation for the contributor made only of its valid contributions.
|
|
1360
1371
|
* @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
|
|
@@ -1810,7 +1821,7 @@ const getFirestoreDatabase = (app) => getFirestore(app);
|
|
|
1810
1821
|
* @param app <FirebaseApp> - the Firebase application.
|
|
1811
1822
|
* @returns <Functions> - the Cloud Functions associated to the application.
|
|
1812
1823
|
*/
|
|
1813
|
-
const getFirebaseFunctions = (app) => getFunctions(app,
|
|
1824
|
+
const getFirebaseFunctions = (app) => getFunctions(app, "europe-west1");
|
|
1814
1825
|
/**
|
|
1815
1826
|
* Retrieve the configuration variables for the AWS services (S3, EC2).
|
|
1816
1827
|
* @returns <AWSVariables> - the values of the AWS services configuration variables.
|
|
@@ -2061,55 +2072,27 @@ const verifyCeremony = async (functions, firestore, ceremonyPrefix, outputDirect
|
|
|
2061
2072
|
};
|
|
2062
2073
|
|
|
2063
2074
|
/**
|
|
2064
|
-
* This function
|
|
2065
|
-
* @param user
|
|
2066
|
-
* @returns
|
|
2067
|
-
*/
|
|
2068
|
-
const getNumberOfPublicReposGitHub = async (user) => {
|
|
2069
|
-
const response = await fetch(`https://api.github.com/user/${user}/repos`, {
|
|
2070
|
-
method: "GET",
|
|
2071
|
-
headers: {
|
|
2072
|
-
Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`
|
|
2073
|
-
}
|
|
2074
|
-
});
|
|
2075
|
-
if (response.status !== 200)
|
|
2076
|
-
throw new Error("It was not possible to retrieve the number of public repositories. Please try again.");
|
|
2077
|
-
const jsonData = await response.json();
|
|
2078
|
-
return jsonData.length;
|
|
2079
|
-
};
|
|
2080
|
-
/**
|
|
2081
|
-
* This function will return the number of followers of a user
|
|
2082
|
-
* @param user <string> The username of the user
|
|
2083
|
-
* @returns <number> The number of followers
|
|
2084
|
-
*/
|
|
2085
|
-
const getNumberOfFollowersGitHub = async (user) => {
|
|
2086
|
-
const response = await fetch(`https://api.github.com/user/${user}/followers`, {
|
|
2087
|
-
method: "GET",
|
|
2088
|
-
headers: {
|
|
2089
|
-
Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`
|
|
2090
|
-
}
|
|
2091
|
-
});
|
|
2092
|
-
if (response.status !== 200)
|
|
2093
|
-
throw new Error("It was not possible to retrieve the number of followers. Please try again.");
|
|
2094
|
-
const jsonData = await response.json();
|
|
2095
|
-
return jsonData.length;
|
|
2096
|
-
};
|
|
2097
|
-
/**
|
|
2098
|
-
* This function will return the number of following of a user
|
|
2099
|
-
* @param user <string> The username of the user
|
|
2100
|
-
* @returns <number> The number of following users
|
|
2075
|
+
* This function queries the GitHub API to fetch users statistics
|
|
2076
|
+
* @param user {string} the user uid
|
|
2077
|
+
* @returns {any} the stats from the GitHub API
|
|
2101
2078
|
*/
|
|
2102
|
-
const
|
|
2103
|
-
const response = await fetch(`https://api.github.com/user/${user}
|
|
2079
|
+
const getGitHubStats = async (user) => {
|
|
2080
|
+
const response = await fetch(`https://api.github.com/user/${user}`, {
|
|
2104
2081
|
method: "GET",
|
|
2105
2082
|
headers: {
|
|
2106
2083
|
Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`
|
|
2107
2084
|
}
|
|
2108
2085
|
});
|
|
2109
2086
|
if (response.status !== 200)
|
|
2110
|
-
throw new Error("It was not possible to retrieve the
|
|
2087
|
+
throw new Error("It was not possible to retrieve the user's statistic. Please try again.");
|
|
2111
2088
|
const jsonData = await response.json();
|
|
2112
|
-
|
|
2089
|
+
const data = {
|
|
2090
|
+
following: jsonData.following,
|
|
2091
|
+
followers: jsonData.followers,
|
|
2092
|
+
publicRepos: jsonData.public_repos,
|
|
2093
|
+
avatarUrl: jsonData.avatar_url
|
|
2094
|
+
};
|
|
2095
|
+
return data;
|
|
2113
2096
|
};
|
|
2114
2097
|
/**
|
|
2115
2098
|
* This function will check if the user is reputable enough to be able to use the app
|
|
@@ -2117,19 +2100,23 @@ const getNumberOfFollowingGitHub = async (user) => {
|
|
|
2117
2100
|
* @param minimumAmountOfFollowing <number> The minimum amount of following the user should have
|
|
2118
2101
|
* @param minimumAmountOfFollowers <number> The minimum amount of followers the user should have
|
|
2119
2102
|
* @param minimumAmountOfPublicRepos <number> The minimum amount of public repos the user should have
|
|
2120
|
-
* @returns <
|
|
2103
|
+
* @returns <any> Return the avatar URL of the user if the user is reputable, false otherwise
|
|
2121
2104
|
*/
|
|
2122
2105
|
const githubReputation = async (userLogin, minimumAmountOfFollowing, minimumAmountOfFollowers, minimumAmountOfPublicRepos) => {
|
|
2123
2106
|
if (!process.env.GITHUB_ACCESS_TOKEN)
|
|
2124
2107
|
throw new Error("The GitHub access token is missing. Please insert a valid token to be used for anti-sybil checks on user registation, and then try again.");
|
|
2125
|
-
const following = await
|
|
2126
|
-
const repos = await getNumberOfPublicReposGitHub(userLogin);
|
|
2127
|
-
const followers = await getNumberOfFollowersGitHub(userLogin);
|
|
2108
|
+
const { following, followers, publicRepos, avatarUrl } = await getGitHubStats(userLogin);
|
|
2128
2109
|
if (following < minimumAmountOfFollowing ||
|
|
2129
|
-
|
|
2110
|
+
publicRepos < minimumAmountOfPublicRepos ||
|
|
2130
2111
|
followers < minimumAmountOfFollowers)
|
|
2131
|
-
return
|
|
2132
|
-
|
|
2112
|
+
return {
|
|
2113
|
+
reputable: false,
|
|
2114
|
+
avatarUrl: ""
|
|
2115
|
+
};
|
|
2116
|
+
return {
|
|
2117
|
+
reputable: true,
|
|
2118
|
+
avatarUrl: avatarUrl
|
|
2119
|
+
};
|
|
2133
2120
|
};
|
|
2134
2121
|
|
|
2135
2122
|
/**
|
|
@@ -2337,8 +2324,13 @@ const vmDependenciesAndCacheArtifactsCommand = (zKeyPath, potPath, snsTopic, reg
|
|
|
2337
2324
|
// eslint-disable-next-line no-template-curly-in-string
|
|
2338
2325
|
"touch ${MARKER_FILE}",
|
|
2339
2326
|
"sudo yum update -y",
|
|
2340
|
-
"curl -
|
|
2341
|
-
"
|
|
2327
|
+
"curl -O https://nodejs.org/dist/v16.13.0/node-v16.13.0-linux-x64.tar.xz",
|
|
2328
|
+
"tar -xf node-v16.13.0-linux-x64.tar.xz",
|
|
2329
|
+
"mv node-v16.13.0-linux-x64 nodejs",
|
|
2330
|
+
"sudo mv nodejs /opt/",
|
|
2331
|
+
"echo 'export NODEJS_HOME=/opt/nodejs' >> /etc/profile",
|
|
2332
|
+
"echo 'export PATH=$NODEJS_HOME/bin:$PATH' >> /etc/profile",
|
|
2333
|
+
"source /etc/profile",
|
|
2342
2334
|
"npm install -g snarkjs",
|
|
2343
2335
|
`aws s3 cp s3://${zKeyPath} /var/tmp/genesisZkey.zkey`,
|
|
2344
2336
|
`aws s3 cp s3://${potPath} /var/tmp/pot.ptau`,
|
|
@@ -2357,6 +2349,7 @@ const vmDependenciesAndCacheArtifactsCommand = (zKeyPath, potPath, snsTopic, reg
|
|
|
2357
2349
|
* @returns Array<string> - the list of commands for contribution verification.
|
|
2358
2350
|
*/
|
|
2359
2351
|
const vmContributionVerificationCommand = (bucketName, lastZkeyStoragePath, verificationTranscriptStoragePathAndFilename) => [
|
|
2352
|
+
`source /etc/profile`,
|
|
2360
2353
|
`aws s3 cp s3://${bucketName}/${lastZkeyStoragePath} /var/tmp/lastZKey.zkey > /var/tmp/log.txt`,
|
|
2361
2354
|
`snarkjs zkvi /var/tmp/genesisZkey.zkey /var/tmp/pot.ptau /var/tmp/lastZKey.zkey > /var/tmp/verification_transcript.log`,
|
|
2362
2355
|
`aws s3 cp /var/tmp/verification_transcript.log s3://${bucketName}/${verificationTranscriptStoragePathAndFilename} &>/dev/null`,
|