@devtion/backend 0.0.0-f3ea056 → 0.0.0-f7df5e1

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/README.md CHANGED
@@ -102,10 +102,10 @@ yarn firebase:init
102
102
 
103
103
  #### AWS Infrastructure
104
104
 
105
- 0. Login or create a [new AWS Account](https://portal.aws.amazon.com/billing/signup?nc2=h_ct&src=header_signup&redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start/email).
105
+ 0. Login or create a [new AWS Account](https://portal.aws.amazon.com/billing/signup?nc2=h_ct&src=header_signup&redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start/email).
106
106
  - The AWS free tier account will cover a good number of requests for ceremonies but there could be some costs based on your ceremony circuits size.
107
- 1. Create an access key for a user with Admin privileges (__NOT ROOT USER__)
108
- 2. Setup the `awscli` ([docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)) and add the keys for this user.
107
+ 1. Create an access key for a user with Admin privileges (**NOT ROOT USER**)
108
+ 2. Setup the `awscli` ([docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)) and add the keys for this user.
109
109
  3. Install `terraform` ([docs](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli))
110
110
  4. Decide on an AWS region (by default this is **us-east-1**) - if you want to change you will need to do the following:
111
111
  1. update **aws/lambda/index.mjs** ([exact line](https://github.com/privacy-scaling-explorations/p0tion/blob/dev/packages/backend/aws/lambda/index.mjs#L3)) to the new region
@@ -117,9 +117,9 @@ yarn firebase:init
117
117
  1. `terraform init`
118
118
  2. `terraform plan`
119
119
  3. `terraform apply`
120
- 4. `terraform output secret_key`
120
+ 4. `terraform output secret_key`
121
121
  - To print the secret access key for the IAM user
122
- 6. Store the other values (sns_topic_arn etc.)
122
+ 5. Store the other values (sns_topic_arn etc.)
123
123
  - These will be needed for the .env file configuration
124
124
 
125
125
  The IAM user created with the steps above can be used for all p0tion's features.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @module @devtion/backend
3
- * @version 1.0.6
3
+ * @version 1.0.8
4
4
  * @file MPC Phase 2 backend for Firebase services management
5
5
  * @copyright Ethereum Foundation 2022
6
6
  * @license MIT
@@ -144,7 +144,8 @@ const SPECIFIC_ERRORS = {
144
144
  SE_VM_FAILED_COMMAND_EXECUTION: makeError("failed-precondition", "VM command execution failed", "Please, contact the coordinator if this error persists."),
145
145
  SE_VM_TIMEDOUT_COMMAND_EXECUTION: makeError("deadline-exceeded", "VM command execution took too long and has been timed-out", "Please, contact the coordinator if this error persists."),
146
146
  SE_VM_CANCELLED_COMMAND_EXECUTION: makeError("cancelled", "VM command execution has been cancelled", "Please, contact the coordinator if this error persists."),
147
- SE_VM_DELAYED_COMMAND_EXECUTION: makeError("unavailable", "VM command execution has been delayed since there were no available instance at the moment", "Please, contact the coordinator if this error persists.")
147
+ SE_VM_DELAYED_COMMAND_EXECUTION: makeError("unavailable", "VM command execution has been delayed since there were no available instance at the moment", "Please, contact the coordinator if this error persists."),
148
+ SE_VM_UNKNOWN_COMMAND_STATUS: makeError("unavailable", "VM command execution has failed due to an unknown status code", "Please, contact the coordinator if this error persists.")
148
149
  };
149
150
  /**
150
151
  * A set of common errors.
@@ -328,7 +329,7 @@ const downloadArtifactFromS3Bucket = async (bucketName, objectKey, localFilePath
328
329
  const writeStream = node_fs.createWriteStream(localFilePath);
329
330
  const streamPipeline = node_util.promisify(node_stream.pipeline);
330
331
  await streamPipeline(response.body, writeStream);
331
- writeStream.on('finish', () => {
332
+ writeStream.on("finish", () => {
332
333
  writeStream.end();
333
334
  });
334
335
  };
@@ -562,7 +563,9 @@ const registerAuthUser = functions__namespace
562
563
  // Delete user
563
564
  await auth.deleteUser(user.uid);
564
565
  // Throw error
565
- logAndThrowError(makeError("permission-denied", "The user is not allowed to sign up because their Github reputation is not high enough.", `The user ${user.displayName === "Null" || user.displayName === null ? user.uid : user.displayName} is not allowed to sign up because their Github reputation is not high enough. Please contact the administrator if you think this is a mistake.`));
566
+ logAndThrowError(makeError("permission-denied", "The user is not allowed to sign up because their Github reputation is not high enough.", `The user ${user.displayName === "Null" || user.displayName === null
567
+ ? user.uid
568
+ : user.displayName} is not allowed to sign up because their Github reputation is not high enough. Please contact the administrator if you think this is a mistake.`));
566
569
  }
567
570
  // store locally
568
571
  avatarUrl = avatarURL;
@@ -577,7 +580,7 @@ const registerAuthUser = functions__namespace
577
580
  }
578
581
  // Set document (nb. we refer to providerData[0] because we use Github OAuth provider only).
579
582
  // In future releases we might want to loop through the providerData array as we support
580
- // more providers.
583
+ // more providers.
581
584
  await userRef.set({
582
585
  name: encodedDisplayName,
583
586
  encodedDisplayName,
@@ -593,7 +596,7 @@ const registerAuthUser = functions__namespace
593
596
  // we want to create a new collection for the users to store the avatars
594
597
  const avatarRef = firestore.collection(actions.commonTerms.collections.avatars.name).doc(uid);
595
598
  await avatarRef.set({
596
- avatarUrl: avatarUrl || "",
599
+ avatarUrl: avatarUrl || ""
597
600
  });
598
601
  printLog(`Authenticated user document with identifier ${uid} has been correctly stored`, LogLevel.DEBUG);
599
602
  printLog(`Authenticated user avatar with identifier ${uid} has been correctly stored`, LogLevel.DEBUG);
@@ -890,7 +893,7 @@ dotenv.config();
890
893
  * @dev true when the participant can participate (1.A, 3.B, 1.D); otherwise false.
891
894
  */
892
895
  const checkParticipantForCeremony = functions__namespace
893
- .region('europe-west1')
896
+ .region("europe-west1")
894
897
  .runWith({
895
898
  memory: "512MB"
896
899
  })
@@ -994,7 +997,7 @@ const checkParticipantForCeremony = functions__namespace
994
997
  * 2) the participant has just finished the contribution for a circuit (contributionProgress != 0 && status = CONTRIBUTED && contributionStep = COMPLETED).
995
998
  */
996
999
  const progressToNextCircuitForContribution = functions__namespace
997
- .region('europe-west1')
1000
+ .region("europe-west1")
998
1001
  .runWith({
999
1002
  memory: "512MB"
1000
1003
  })
@@ -1041,7 +1044,7 @@ const progressToNextCircuitForContribution = functions__namespace
1041
1044
  * 5) Completed contribution computation and verification.
1042
1045
  */
1043
1046
  const progressToNextContributionStep = functions__namespace
1044
- .region('europe-west1')
1047
+ .region("europe-west1")
1045
1048
  .runWith({
1046
1049
  memory: "512MB"
1047
1050
  })
@@ -1092,7 +1095,7 @@ const progressToNextContributionStep = functions__namespace
1092
1095
  * @dev enable the current contributor to resume a contribution from where it had left off.
1093
1096
  */
1094
1097
  const permanentlyStoreCurrentContributionTimeAndHash = functions__namespace
1095
- .region('europe-west1')
1098
+ .region("europe-west1")
1096
1099
  .runWith({
1097
1100
  memory: "512MB"
1098
1101
  })
@@ -1134,7 +1137,7 @@ const permanentlyStoreCurrentContributionTimeAndHash = functions__namespace
1134
1137
  * @dev enable the current contributor to resume a multi-part upload from where it had left off.
1135
1138
  */
1136
1139
  const temporaryStoreCurrentContributionMultiPartUploadId = functions__namespace
1137
- .region('europe-west1')
1140
+ .region("europe-west1")
1138
1141
  .runWith({
1139
1142
  memory: "512MB"
1140
1143
  })
@@ -1172,7 +1175,7 @@ const temporaryStoreCurrentContributionMultiPartUploadId = functions__namespace
1172
1175
  * @dev enable the current contributor to resume a multi-part upload from where it had left off.
1173
1176
  */
1174
1177
  const temporaryStoreCurrentContributionUploadedChunkData = functions__namespace
1175
- .region('europe-west1')
1178
+ .region("europe-west1")
1176
1179
  .runWith({
1177
1180
  memory: "512MB"
1178
1181
  })
@@ -1214,7 +1217,7 @@ const temporaryStoreCurrentContributionUploadedChunkData = functions__namespace
1214
1217
  * contributed to every selected ceremony circuits (= DONE).
1215
1218
  */
1216
1219
  const checkAndPrepareCoordinatorForFinalization = functions__namespace
1217
- .region('europe-west1')
1220
+ .region("europe-west1")
1218
1221
  .runWith({
1219
1222
  memory: "512MB"
1220
1223
  })
@@ -1366,39 +1369,54 @@ const coordinate = async (participant, circuit, isSingleParticipantCoordination,
1366
1369
  * Wait until the command has completed its execution inside the VM.
1367
1370
  * @dev this method implements a custom interval to check 5 times after 1 minute if the command execution
1368
1371
  * has been completed or not by calling the `retrieveCommandStatus` method.
1369
- * @param {any} resolve the promise.
1370
- * @param {any} reject the promise.
1371
1372
  * @param {SSMClient} ssm the SSM client.
1372
1373
  * @param {string} vmInstanceId the unique identifier of the VM instance.
1373
1374
  * @param {string} commandId the unique identifier of the VM command.
1374
1375
  * @returns <Promise<void>> true when the command execution succeed; otherwise false.
1375
1376
  */
1376
- const waitForVMCommandExecution = (resolve, reject, ssm, vmInstanceId, commandId) => {
1377
- const interval = setInterval(async () => {
1377
+ const waitForVMCommandExecution = (ssm, vmInstanceId, commandId) => new Promise((resolve, reject) => {
1378
+ const poll = async () => {
1378
1379
  try {
1379
1380
  // Get command status.
1380
1381
  const cmdStatus = await actions.retrieveCommandStatus(ssm, vmInstanceId, commandId);
1381
1382
  printLog(`Checking command ${commandId} status => ${cmdStatus}`, LogLevel.DEBUG);
1382
- if (cmdStatus === clientSsm.CommandInvocationStatus.SUCCESS) {
1383
- printLog(`Command ${commandId} successfully completed`, LogLevel.DEBUG);
1384
- // Resolve the promise.
1385
- resolve();
1386
- }
1387
- else if (cmdStatus === clientSsm.CommandInvocationStatus.FAILED) {
1388
- logAndThrowError(SPECIFIC_ERRORS.SE_VM_FAILED_COMMAND_EXECUTION);
1389
- reject();
1390
- }
1391
- else if (cmdStatus === clientSsm.CommandInvocationStatus.TIMED_OUT) {
1392
- logAndThrowError(SPECIFIC_ERRORS.SE_VM_TIMEDOUT_COMMAND_EXECUTION);
1393
- reject();
1394
- }
1395
- else if (cmdStatus === clientSsm.CommandInvocationStatus.CANCELLED) {
1396
- logAndThrowError(SPECIFIC_ERRORS.SE_VM_CANCELLED_COMMAND_EXECUTION);
1397
- reject();
1383
+ let error;
1384
+ switch (cmdStatus) {
1385
+ case clientSsm.CommandInvocationStatus.CANCELLING:
1386
+ case clientSsm.CommandInvocationStatus.CANCELLED: {
1387
+ error = SPECIFIC_ERRORS.SE_VM_CANCELLED_COMMAND_EXECUTION;
1388
+ break;
1389
+ }
1390
+ case clientSsm.CommandInvocationStatus.DELAYED: {
1391
+ error = SPECIFIC_ERRORS.SE_VM_DELAYED_COMMAND_EXECUTION;
1392
+ break;
1393
+ }
1394
+ case clientSsm.CommandInvocationStatus.FAILED: {
1395
+ error = SPECIFIC_ERRORS.SE_VM_FAILED_COMMAND_EXECUTION;
1396
+ break;
1397
+ }
1398
+ case clientSsm.CommandInvocationStatus.TIMED_OUT: {
1399
+ error = SPECIFIC_ERRORS.SE_VM_TIMEDOUT_COMMAND_EXECUTION;
1400
+ break;
1401
+ }
1402
+ case clientSsm.CommandInvocationStatus.IN_PROGRESS:
1403
+ case clientSsm.CommandInvocationStatus.PENDING: {
1404
+ // wait a minute and poll again
1405
+ setTimeout(poll, 60000);
1406
+ return;
1407
+ }
1408
+ case clientSsm.CommandInvocationStatus.SUCCESS: {
1409
+ printLog(`Command ${commandId} successfully completed`, LogLevel.DEBUG);
1410
+ // Resolve the promise.
1411
+ resolve();
1412
+ return;
1413
+ }
1414
+ default: {
1415
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_UNKNOWN_COMMAND_STATUS);
1416
+ }
1398
1417
  }
1399
- else if (cmdStatus === clientSsm.CommandInvocationStatus.DELAYED) {
1400
- logAndThrowError(SPECIFIC_ERRORS.SE_VM_DELAYED_COMMAND_EXECUTION);
1401
- reject();
1418
+ if (error) {
1419
+ logAndThrowError(error);
1402
1420
  }
1403
1421
  }
1404
1422
  catch (error) {
@@ -1408,12 +1426,9 @@ const waitForVMCommandExecution = (resolve, reject, ssm, vmInstanceId, commandId
1408
1426
  // Reject the promise.
1409
1427
  reject();
1410
1428
  }
1411
- finally {
1412
- // Clear the interval.
1413
- clearInterval(interval);
1414
- }
1415
- }, 60000); // 1 minute.
1416
- };
1429
+ };
1430
+ setTimeout(poll, 60000);
1431
+ });
1417
1432
  /**
1418
1433
  * This method is used to coordinate the waiting queues of ceremony circuits.
1419
1434
  * @dev this cloud function is triggered whenever an update of a document related to a participant of a ceremony occurs.
@@ -1434,7 +1449,7 @@ const waitForVMCommandExecution = (resolve, reject, ssm, vmInstanceId, commandId
1434
1449
  * - Just completed a contribution or all contributions for each circuit. If yes, coordinate (multi-participant scenario).
1435
1450
  */
1436
1451
  const coordinateCeremonyParticipant = functionsV1__namespace
1437
- .region('europe-west1')
1452
+ .region("europe-west1")
1438
1453
  .runWith({
1439
1454
  memory: "512MB"
1440
1455
  })
@@ -1537,7 +1552,7 @@ const checkIfVMRunning = async (ec2, vmInstanceId, attempts = 5) => {
1537
1552
  * 1.A.4.C.1) If true, update circuit waiting for queue and average timings accordingly to contribution verification results;
1538
1553
  * 2) Send all updates atomically to the Firestore database.
1539
1554
  */
1540
- const verifycontribution = functionsV2__namespace.https.onCall({ memory: "16GiB", timeoutSeconds: 3600, region: 'europe-west1' }, async (request) => {
1555
+ const verifycontribution = functionsV2__namespace.https.onCall({ memory: "16GiB", timeoutSeconds: 3600, region: "europe-west1" }, async (request) => {
1541
1556
  if (!request.auth || (!request.auth.token.participant && !request.auth.token.coordinator))
1542
1557
  logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
1543
1558
  if (!request.data.ceremonyId ||
@@ -1725,7 +1740,7 @@ const verifycontribution = functionsV2__namespace.https.onCall({ memory: "16GiB"
1725
1740
  const newAvgVerifyCloudFunctionTime = avgVerifyCloudFunctionTime > 0
1726
1741
  ? (avgVerifyCloudFunctionTime + verifyCloudFunctionTime) / 2
1727
1742
  : verifyCloudFunctionTime;
1728
- // Prepare tx to update circuit average contribution/verification time.
1743
+ // Prepare tx to update circuit average contribution/verification time.
1729
1744
  const updatedCircuitDoc = await getDocumentById(actions.getCircuitsCollectionPath(ceremonyId), circuitId);
1730
1745
  const { waitingQueue: updatedWaitingQueue } = updatedCircuitDoc.data();
1731
1746
  /// @dev this must happen only for valid contributions.
@@ -1775,7 +1790,7 @@ const verifycontribution = functionsV2__namespace.https.onCall({ memory: "16GiB"
1775
1790
  commandId = await actions.runCommandUsingSSM(ssm, vmInstanceId, verificationCommand);
1776
1791
  printLog(`Starting the execution of command ${commandId}`, LogLevel.DEBUG);
1777
1792
  // Step (1.A.3.3).
1778
- return new Promise((resolve, reject) => waitForVMCommandExecution(resolve, reject, ssm, vmInstanceId, commandId))
1793
+ return waitForVMCommandExecution(ssm, vmInstanceId, commandId)
1779
1794
  .then(async () => {
1780
1795
  // Command execution successfully completed.
1781
1796
  printLog(`Command ${commandId} execution has been successfully completed`, LogLevel.DEBUG);
@@ -1787,40 +1802,38 @@ const verifycontribution = functionsV2__namespace.https.onCall({ memory: "16GiB"
1787
1802
  logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION);
1788
1803
  });
1789
1804
  }
1790
- else {
1791
- // CF approach.
1792
- printLog(`CF mechanism`, LogLevel.DEBUG);
1793
- const potStoragePath = actions.getPotStorageFilePath(files.potFilename);
1794
- const firstZkeyStoragePath = actions.getZkeyStorageFilePath(prefix, `${prefix}_${actions.genesisZkeyIndex}.zkey`);
1795
- // Prepare temporary file paths.
1796
- // (nb. these are needed to download the necessary artifacts for verification from AWS S3).
1797
- verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(verificationTranscriptCompleteFilename);
1798
- const potTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}.pot`);
1799
- const firstZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_genesis.zkey`);
1800
- const lastZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_last.zkey`);
1801
- // Create and populate transcript.
1802
- const transcriptLogger = actions.createCustomLoggerForFile(verificationTranscriptTemporaryLocalPath);
1803
- transcriptLogger.info(`${isFinalizing ? `Final verification` : `Verification`} transcript for ${prefix} circuit Phase 2 contribution.\n${isFinalizing ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}`} (${contributorOrCoordinatorIdentifier})\n`);
1804
- // Step (1.A.2).
1805
- await downloadArtifactFromS3Bucket(bucketName, potStoragePath, potTempFilePath);
1806
- await downloadArtifactFromS3Bucket(bucketName, firstZkeyStoragePath, firstZkeyTempFilePath);
1807
- await downloadArtifactFromS3Bucket(bucketName, lastZkeyStoragePath, lastZkeyTempFilePath);
1808
- // Step (1.A.4).
1809
- isContributionValid = await snarkjs.zKey.verifyFromInit(firstZkeyTempFilePath, potTempFilePath, lastZkeyTempFilePath, transcriptLogger);
1810
- // Compute contribution hash.
1811
- lastZkeyBlake2bHash = await actions.blake512FromPath(lastZkeyTempFilePath);
1812
- // Free resources by unlinking temporary folders.
1813
- // Do not free-up verification transcript path here.
1814
- try {
1815
- fs.unlinkSync(potTempFilePath);
1816
- fs.unlinkSync(firstZkeyTempFilePath);
1817
- fs.unlinkSync(lastZkeyTempFilePath);
1818
- }
1819
- catch (error) {
1820
- printLog(`Error while unlinking temporary files - Error ${error}`, LogLevel.WARN);
1821
- }
1822
- await completeVerification();
1805
+ // CF approach.
1806
+ printLog(`CF mechanism`, LogLevel.DEBUG);
1807
+ const potStoragePath = actions.getPotStorageFilePath(files.potFilename);
1808
+ const firstZkeyStoragePath = actions.getZkeyStorageFilePath(prefix, `${prefix}_${actions.genesisZkeyIndex}.zkey`);
1809
+ // Prepare temporary file paths.
1810
+ // (nb. these are needed to download the necessary artifacts for verification from AWS S3).
1811
+ verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(verificationTranscriptCompleteFilename);
1812
+ const potTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}.pot`);
1813
+ const firstZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_genesis.zkey`);
1814
+ const lastZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_last.zkey`);
1815
+ // Create and populate transcript.
1816
+ const transcriptLogger = actions.createCustomLoggerForFile(verificationTranscriptTemporaryLocalPath);
1817
+ transcriptLogger.info(`${isFinalizing ? `Final verification` : `Verification`} transcript for ${prefix} circuit Phase 2 contribution.\n${isFinalizing ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}`} (${contributorOrCoordinatorIdentifier})\n`);
1818
+ // Step (1.A.2).
1819
+ await downloadArtifactFromS3Bucket(bucketName, potStoragePath, potTempFilePath);
1820
+ await downloadArtifactFromS3Bucket(bucketName, firstZkeyStoragePath, firstZkeyTempFilePath);
1821
+ await downloadArtifactFromS3Bucket(bucketName, lastZkeyStoragePath, lastZkeyTempFilePath);
1822
+ // Step (1.A.4).
1823
+ isContributionValid = await snarkjs.zKey.verifyFromInit(firstZkeyTempFilePath, potTempFilePath, lastZkeyTempFilePath, transcriptLogger);
1824
+ // Compute contribution hash.
1825
+ lastZkeyBlake2bHash = await actions.blake512FromPath(lastZkeyTempFilePath);
1826
+ // Free resources by unlinking temporary folders.
1827
+ // Do not free-up verification transcript path here.
1828
+ try {
1829
+ fs.unlinkSync(potTempFilePath);
1830
+ fs.unlinkSync(firstZkeyTempFilePath);
1831
+ fs.unlinkSync(lastZkeyTempFilePath);
1823
1832
  }
1833
+ catch (error) {
1834
+ printLog(`Error while unlinking temporary files - Error ${error}`, LogLevel.WARN);
1835
+ }
1836
+ await completeVerification();
1824
1837
  }
1825
1838
  });
1826
1839
  /**
@@ -1829,7 +1842,7 @@ const verifycontribution = functionsV2__namespace.https.onCall({ memory: "16GiB"
1829
1842
  * this does not happen if the participant is actually the coordinator who is finalizing the ceremony.
1830
1843
  */
1831
1844
  const refreshParticipantAfterContributionVerification = functionsV1__namespace
1832
- .region('europe-west1')
1845
+ .region("europe-west1")
1833
1846
  .runWith({
1834
1847
  memory: "512MB"
1835
1848
  })
@@ -1890,7 +1903,7 @@ const refreshParticipantAfterContributionVerification = functionsV1__namespace
1890
1903
  * and verification key extracted from the circuit final contribution (as part of the ceremony finalization process).
1891
1904
  */
1892
1905
  const finalizeCircuit = functionsV1__namespace
1893
- .region('europe-west1')
1906
+ .region("europe-west1")
1894
1907
  .runWith({
1895
1908
  memory: "512MB"
1896
1909
  })
@@ -2087,8 +2100,10 @@ const createBucket = functions__namespace
2087
2100
  CORSConfiguration: {
2088
2101
  CORSRules: [
2089
2102
  {
2090
- AllowedMethods: ["GET"],
2091
- AllowedOrigins: ["*"]
2103
+ AllowedMethods: ["GET", "PUT"],
2104
+ AllowedOrigins: ["*"],
2105
+ ExposeHeaders: ["ETag", "Content-Length"],
2106
+ AllowedHeaders: ["*"]
2092
2107
  }
2093
2108
  ]
2094
2109
  }