dcp-client 4.2.0 → 4.2.1

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.
@@ -3821,7 +3821,7 @@ eval("// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission
3821
3821
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
3822
3822
 
3823
3823
  "use strict";
3824
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"5952aad9fbfe61afed37adabad23f30796fa5f5c\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.0\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220404\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9d077e0cbe7eca6d39eff2d20ac5c7f792d93098\"},\"built\":\"Tue Apr 05 2022 16:21:10 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Tue 05 Apr 2022 04:21:10 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.11\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3824
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"f6edd2d1ee975ba6c7161426873f38cbfe50f23c\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220404\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\"},\"built\":\"Wed Apr 06 2022 15:53:41 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Wed 06 Apr 2022 03:53:41 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.11\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3825
3825
 
3826
3826
  /***/ }),
3827
3827
 
@@ -4125,7 +4125,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
4125
4125
  \**********************************************/
4126
4126
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4127
4127
 
4128
- eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\n * \n * All shared functions among the modals.\n */\nconst { fetchRelative } = __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nexports.OnCloseErrorCode = 'DCP_CM:CANCELX';\n\nif (DCP_ENV.isBrowserPlatform) {\n // Provide as export for the convenience of `utils.MicroModal` instead of a separate require.\n exports.MicroModal = __webpack_require__(/*! micromodal */ \"./node_modules/micromodal/dist/micromodal.es.js\")[\"default\"];\n}\n\n/**\n * Return a unique string, formatted as a GET parameter, that changes often enough to\n * always force the browser to fetch the latest version of our resource.\n *\n * @note Currently always returns the Date-based poison due to webpack. \n */\nfunction cachePoison() {\n if (true)\n return '?ucp=5952aad9fbfe61afed37adabad23f30796fa5f5c'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === (__webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript)) {\n let url = new ((__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL))(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/client-modal/utils.js?");
4128
+ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\n * \n * All shared functions among the modals.\n */\nconst { fetchRelative } = __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nexports.OnCloseErrorCode = 'DCP_CM:CANCELX';\n\nif (DCP_ENV.isBrowserPlatform) {\n // Provide as export for the convenience of `utils.MicroModal` instead of a separate require.\n exports.MicroModal = __webpack_require__(/*! micromodal */ \"./node_modules/micromodal/dist/micromodal.es.js\")[\"default\"];\n}\n\n/**\n * Return a unique string, formatted as a GET parameter, that changes often enough to\n * always force the browser to fetch the latest version of our resource.\n *\n * @note Currently always returns the Date-based poison due to webpack. \n */\nfunction cachePoison() {\n if (true)\n return '?ucp=f6edd2d1ee975ba6c7161426873f38cbfe50f23c'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === (__webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript)) {\n let url = new ((__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL))(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/client-modal/utils.js?");
4129
4129
 
4130
4130
  /***/ }),
4131
4131
 
@@ -4155,7 +4155,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
4155
4155
  \*********************************/
4156
4156
  /***/ ((module, exports, __webpack_require__) => {
4157
4157
 
4158
- eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : 0\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = (__webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory)(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': (__webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute),\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-wallet': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet),\n 'ethereumjs-util': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util),\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"5952aad9fbfe61afed37adabad23f30796fa5f5c\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.0\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220404\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9d077e0cbe7eca6d39eff2d20ac5c7f792d93098\"},\"built\":\"Tue Apr 05 2022 16:21:10 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Tue 05 Apr 2022 04:21:10 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.11\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': (__webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4158
+ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : 0\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = (__webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory)(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': (__webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute),\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-wallet': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet),\n 'ethereumjs-util': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util),\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"f6edd2d1ee975ba6c7161426873f38cbfe50f23c\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220404\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\"},\"built\":\"Wed Apr 06 2022 15:53:41 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Wed 06 Apr 2022 03:53:41 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.11\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': (__webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4159
4159
 
4160
4160
  /***/ }),
4161
4161
 
@@ -4257,7 +4257,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4257
4257
  \*************************************************/
4258
4258
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4259
4259
 
4260
- eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=5952aad9fbfe61afed37adabad23f30796fa5f5c,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg-web.js?");
4260
+ eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=f6edd2d1ee975ba6c7161426873f38cbfe50f23c,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg-web.js?");
4261
4261
 
4262
4262
  /***/ }),
4263
4263
 
@@ -4518,7 +4518,7 @@ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan
4518
4518
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4519
4519
 
4520
4520
  "use strict";
4521
- eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @author Wes Garland\n * @date January 2020, Feb 2021, Mar 2022\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established disconnected close-wait closing closed\n * =====================================================================================================================================\n * |-- i:connect ---->\n * |-- t:newTarget -->\n * X--------------------------------------------------------------------------------> doClose()\n * |-- transportDisconnectHandler -------------------------->\n * |-- i:connect ---------->\n * |-- t:establishTarget -->\n * |-- transportDisconnectHandler -->\n * <-- reconnect -------------------|\n * X--------- doClose() ------->\n * X- doClose() ->\n * XXX------------|---------------------|--------------|-----------------------------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n * \n * Error Codes relevant to DCP Connections:\n * DCPC-1001 - CONNECTION CANNOT SEND WHEN CLOSED\n * DCPC-1002 - MESSAGE CAME FROM INVALID SENDER\n * DCPC-1003 - MESSAGE SIGNATURE INVALID \n * DCPC-1004 - TRYING TO CONNECT AFTER ALREADY CONNECTED\n * DCPC-1005 - TRYING TO ESTABLISH TARGET AFTER TARGET ALREADY ESTABLISHED\n * DCPC-1006 - CONNECTION COULD NOT BE ESTABLISHED WITHIN 30 SECONDS\n * DCPC-1007 - RECEIVED MESSAGE PAYLOAD BEFORE CONNECT OPERATION\n * DCPC-1008 - TARGET RESPONDED WITH INVALID DCPSID\n * DCPC-1009 - MESSAGE IS OF UNKNOWN TYPE\n * DCPC-1010 - DUPLICATE TRANSMISSION RECEIPT\n * DCPC-1011 - DEFAULT ERROR CODE WHEN PEER SENDS CLOSE MESSAGE\n * DCPC-1012 - MESSAGE IS OF TYPE 'UNHANDLED MESSAGE'\n * DCPC-1013 - MESSAGE IS INVALID\n * DCPC-1014 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF ERROR\n */\n\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./connection-message */ \"./src/protocol-v4/connection/connection-message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nconst isDebugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting', /* initiator: establish first transport instance connection; target: listening */\n 'established',\n 'disconnected', /* connection is still valid, but underlying transport is no longer connected */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n var _role;\n \n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n\n if (target)\n _role = role.initiator;\n else\n _role = role.target;\n \n super(`Protocol Connection (${role})`);\n this.role = _role;\n \n if (target) {\n this.debugLabel = 'connection(i):';\n this._target = target;\n this.hasNtp = false;\n } else {\n this.debugLabel = 'connection(t):';\n this.hasNtp = true;\n }\n\n if (idKeystore) {\n this.identityPromise = Promise.resolve(idKeystore);\n } else {\n /* Always resolved by the time a session is established */\n debugging('connection') && console.debug('loading identity from wallet');\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n });\n\n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n\n this.receiver = new Receiver(this, this.messageLedger);\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n /* Create a connection config as this.connectionOptions which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n *\n * Having this as an exposed method instead of hidden in the constructor\n * is due to the lazy determination of the connection url.\n */\n this.configureConnectionForUrl = (url) => {\n this.url = url;\n this.connectionOptions = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'allowBatch': true,\n 'maxMessagesPerBatch': 100,\n 'identityUnlockTimeout': 300,\n 'ttl': {\n 'min': 15,\n 'max': 600,\n 'default': 120\n },\n 'transports': [ 'socketio' ],\n }),\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.role === role.initiator ? this.url.href : 'target'],\n connectionOptions\n );\n \n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 3000 : 20000); /** XXXwg make this configurable */\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n this.loggableDest = this.role === role.initiator ? this.url : '<target>';\n }\n\n /* By default, unsent messages cause .send() to reject for DCP intiators, but not targets. When\n * messages are unsent but not rejected, the send promise resolves with an instance of Error.\n *\n * Note: \"unsent messages\" are messages we tried to send, but couldn't be verified as sent because \n * the connection closed. It is plausible that they reached the other end, but also plausible that\n * they did not.\n *\n * @note XXX this is expedient, but not really correct. DCP is supposed to be completely peer-to-peer;\n * what we need to do is unify around one way or the other of handling unsent messages (probably\n * rejection), but -- importantly -- the daemons need to cross their Ts and dot their Is when it\n * comes to handling this stuff. My current thinking is that we could should use a specific DCP\n * error code for that, and make it non-fatal at the unhandledRejection when it's for a response,\n * but not a command.\n */\n this.rejectUnsentMessages = this.role === role.initiator;\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n *\n * @note this API is wrong. It should be using DCP Config fragments instead of (url,ks) /wg Mar 2022\n */\n static async newTarget(url, ks, transport) {\n const pk = await ks.getPrivateKey();\n const ksUnlocked = await new wallet.Keystore(pk, '') /* needed for daemon operation */\n const target = new Connection(undefined, ksUnlocked); \n\n assert(target.role === role.target);\n target._target = { location: url };\n target.transport = transport;\n\n await target.doPreConnectTasks();\n\n target.state.set('initial', 'connecting'); /* connecting => listen */\n return target;\n }\n\n /**\n * Non-API function which is mostly a design wart. This needs to be invoked\n * - after we know the connection URL\n * - before we make a message\n * - in an async way because there is a DNS lookup\n */\n async doPreConnectTasks()\n {\n if (!this.state.is('connecting') || this.sender)\n return;\n\n if (this.role === role.initiator && this._target.hasOwnProperty('friendLocation') && await a$isFriendlyUrl(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n \n this.sender = new Sender(this); // create sender before promises so that we can still enqueue messages before hopping off the event loop\n }\n \n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect()\n {\n if (this.state.is('initial'))\n {\n this.connectPromise = this.a$_connect();\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n this.connectPromise = this.a$_reconnect();\n return this.connectPromise;\n }\n \n if (this.state.is('connecting'))\n {\n assert(this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('established'))\n return;\n \n if (this.state.in(['closed', 'close-wait', 'closing']))\n throw new Error('connection already closed', 'DCPC-1016');\n\n throw new Error('impossible');\n }\n\n /**\n * Performs a reconnection for connections which are in the disconnected state, and\n * tries to send any in-flight or enqueued messages as soon as that happens.\n */\n async a$_reconnect()\n {\n assert(this.state.is('disconnected'));\n\n this.state.set('disconnected', 'connecting');\n debugging() && console.log(`391: entering a$connectToTarget...`);\n const connected = await this.a$connectToTarget();\n\n // If we didn't connect / bailed early, that suggests we collided with\n // another reconnect handler, so this attempt can be abandoned\n if (connected === false) {\n debugging('connection') && console.log(this.debugLabel, `396: Aborted extra reconnection attempt`);\n return;\n }\n\n this.state.set('connecting', 'established');\n \n debugging('connection') && console.log(this.debugLabel, `402: Reconnected`);\n\n this.emit('connect'); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n\n async a$_connect() {\n var presharedPeerAddress;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n\n // This has to happen after updating the state, or we get races due to \n // \"test->async->act on test result\" races\n await this.doPreConnectTasks();\n\n await this.a$connectToTarget();\n const establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session over ${this.transport.name}:`, error);\n this.close(error, true);\n throw error;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n let identity = await this._target.identity;\n\n if ( false\n || typeof identity !== 'object'\n || typeof identity.address !== 'object'\n || !(identity.address instanceof wallet.Address))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug(this.debugLabel, 'Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n\n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.notifyTransportReady();\n }\n\n /**\n * unreference any objects entrained by this connection so that it does not prevent\n * the node program from exiting naturally.\n */\n unref()\n {\n if (this.connectAbortTimer && this.connectAbortTimer.unref)\n this.connectAbortTimer.unref();\n }\n\n /**\n * Method is invoked when the transport disconnects. Transport instance is responsible for its own\n * finalization; Connection instance is responsible for finding a new transport, resuming the\n * connection, and retransmitting any in-flight message.\n */\n transportDisconnectHandler()\n {\n try\n { \n if (this.state.in(['disconnected', 'closing', 'close-wait', 'closed'])) /* transports may fire this more than once */\n return;\n \n this.state.set(['connecting', 'established'], 'disconnected');\n this.emit('disconnect'); /* UI hint: \"internet unavailable\" */\n debugging('connection') && console.debug(this.debugLabel, `Transport disconnected from ${this.url}; ${this.sender.inFlight ? 'have' : 'no'} in-flight message`);\n\n if (!this.dcpsid)\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\n \n if (this.role === role.target)\n {\n /* targets generally can't reconnect due to NAT */\n debugging('connection') && console.debug(this.debugLabel, `Waiting for initiator to reconnect for ${this.dcpsid}`);\n return;\n }\n\n if (!this.sender.inFlight && this.connectionOptions.onDemand)\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n else\n this.connect();\n }\n catch(error)\n {\n debugging('connection') && console.debug(error);\n this.close(error, true);\n\n if (error.code !== 'DCPC-1016')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target\n * - Rejects when we give up on all transports.\n * - Resolves when we have connected to target using a transport.\n *\n * The connection attempt will keep a node program \"alive\" while it is happening.\n * The `autoUnref` connectionOption and unref() methods offer ways to make this not\n * happen.\n */\n async a$connectToTarget()\n {\n const that = this;\n const availableTransports = [].concat(this.connectionOptions.transports);\n var quitMsg = false; /* not falsey => reject asap, value is error message */\n var quitCode = undefined;\n var boSleepIntr; /* if not falsey, a function that interrupts the backoff sleep */\n var transportConnectIntr; /* if not falsey, a function that interrupts the current connection attempt */\n\n // If there is already a connectAbortTimer, then we should signal the caller\n // that we were called in error\n if (this.connectAbortTimer)\n return false;\n\n /* This timer has the lifetime of the entire connection attempt. When we time out,\n * we set the quitMsg to get the retry loop to quit, then we interrupt the timer so\n * that we don't have to wait for the current backoff to expire before we notice, and\n * we expire the current attempt to connect right away as well.\n */\n this.connectAbortTimer = setTimeout(() => {\n quitMsg = 'connection timeout';\n if (boSleepIntr) boSleepIntr();\n if (transportConnectIntr) transportConnectIntr();\n }, this.connectionOptions.connectTimeout * 1000);\n\n if (this.connectionOptions.autoUnref)\n this.unref();\n\n /* cleanup code called on return/throw */\n function cleanup_ctt()\n {\n clearTimeout(that.connectAbortTimer);\n delete that.connectAbortTimer;\n }\n\n /* Connect to target with a specific transport. Resolves with { bool success, obj transport } */\n function a$connectWithTransport(transportName)\n { \n transportConnectIntr = false;\n\n return new Promise((connectWithTransport_resolve, connectWithTransport_reject) => { \n const TransportClass = Transport.require(transportName);\n const transport = new TransportClass(that.url, Object.assign({ connectionId: that.id }, that.connectionOptions[transportName]));\n var ret = { transport };\n\n function cleanup_cwt()\n {\n for (let eventName of transport.eventNames())\n for (let listener of transport.listeners(eventName))\n transport.off(eventName, listener);\n }\n \n /* In the case where we have a race condition in the transport implementation, arrange things\n * so that we resolve with whatever fired last if we have a double-fire on the same pass of \n * the event loop.\n */\n transport.on('connect', () => { cleanup_cwt(); ret.success=true; connectWithTransport_resolve(ret) });\n transport.on('error', (error) => { cleanup_cwt(); connectWithTransport_reject(error) });\n transport.on('connect-failed', (error) => {\n cleanup_cwt();\n ret.success = false;\n ret.error = error;\n debugging() && console.log(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close(true) };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1015'));\n }\n \n /* Loop while trying each available transport in turn. Sleep with exponential backoff between runs */\n while (!quitMsg)\n {\n for (let transportName of availableTransports)\n {\n try\n {\n const { success, error, transport } = await a$connectWithTransport(transportName);\n \n if (success === true)\n { /* have successfully connected to target */\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler());\n transport.on('close', () => this.transportDisconnectHandler());\n\n transportConnectIntr = false;\n cleanup_ctt();\n\n this.transport = transport;\n // a connect event will be emitted in the caller, as well as a\n // call to this.sender.notifyTransportReady();\n \n return true; \n }\n\n if (error && error.httpStatus)\n {\n switch(error.httpStatus)\n {\n case 301: case 302: case 303: case 307: case 308:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n break;\n case 400: case 403: case 404:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n default:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; will try again with ${transportName}`);\n break;\n }\n }\n }\n catch (impossbleError)\n {\n /* transport connection attempts should never throw. */\n debugging('connection') && console.debug(this.debugLabel, `Error connecting to ${this.url} with ${transportName}; won't try again:`, impossibleError);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n }\n }\n \n if (availableTransports.length === 0)\n {\n quitMsg = 'all transports exhausted';\n break;\n }\n \n /* Go to (interruptible) sleep for a while before trying again */\n const backoffTimeMs = this.backoffTimeIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'trying again in', Number(backoffTimeMs / 1000).toFixed(2), 'seconds');\n const boSleepPromise = a$sleepMs(backoffTimeMs);\n boSleepIntr = boSleepPromise.intr;\n await boSleepPromise;\n boSleepIntr = false;\n } \n\n /* The only way we get here is for us to discover that the connection is unconnectable - eg \n * reject timer has expired or similar.\n */\n cleanup_ctt();\n throw new DCPError(quitMsg, 'DCPC-1016', quitCode);\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n assert(this.role === role.target);\n \n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid; \n this.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n *\n * XXXwg this code needs an audit re error handling: what message error should we be emitting?\n * why do we keep working after we find an error?\n *\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n var messageError;\n var messageValid = true;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n\n try\n {\n message = typeof messageJSON === 'object' ? messageJSON : JSON.parse(messageJSON);\n }\n catch(error)\n {\n console.error('connection::handleMessage received unparseable message from peer:', error);\n this.emit('error', error);\n return;\n }\n \n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, `duplicate message nonce=${message.body.nonce}:`, message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `received message ${message.body.type} ${message.body.id}; nonce=`, message.body.nonce);\n\n /* Capture the initial identity of the remote end during the connect operation */\n if (this.authorizedSender === null)\n {\n let messageBody = message.body;\n let payload = messageBody.payload;\n \n if (payload && message.body.type === 'batch')\n {\n for (let i=0; i < payload.length; i++)\n {\n let innerMessageBody = payload[i];\n\n if (innerMessageBody.payload && innerMessageBody.payload.operation === 'connect' && (innerMessageBody.type === 'response' || innerMessageBody.type === 'request'))\n {\n messageBody = innerMessageBody;\n payload = innerMessageBody.payload;\n break;\n }\n }\n }\n\n if (payload)\n {\n if (payload.operation === 'connect' && (messageBody.type === 'response' || messageBody.type === 'request'))\n this.authorizedSender = message.owner;\n else\n throw new DCPError('Message payload received before connection operation', 'DCPC-1007');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n messageError = new DCPError('Message came from invalid sender.', 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message owner was not an authorized sender - aborting connection');\n this.close(messageError, true);\n this.emit('error', messageError);\n return;\n }\n }\n\n if (this.role === role.target && this.state.in(['connecting']))\n {\n await this.doPreConnectTasks();\n\n // while connecting, the target gets its nonce from the initiator\n this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message signature: ${validation.errorMessage}`, 'DCPC-1003');\n debugging('connection') && console.debug(this.debugLabel, 'Message signature failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;\n }\n\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n messageError = new DCPError(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`,'DCPC-1012');\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(messageError, true);\n messageValid = false;\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1013');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;;\n }\n\n if (!messageValid) {\n message.body.type = 'unhandled-message'\n this.emit('error', messageError);\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, `acking message. ${message.body.id}; token=`, message.body.ackToken);\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n try {\n this.transport.send(signedMessage);\n }\n catch(error) {\n console.warn(this.debugLabel, `Failed to ack message ${message.id}: `, error.message);\n };\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateSignature(message)\n {\n if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\n \n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n\n if (!signatureValid)\n {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessage(message)\n {\n try\n {\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message signature does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce)\n {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' }; // eslint-disable-line no-unreachable\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command\n */\n closeWait (errorCode = null)\n {\n const preCloseState = this.state.valueOf();\n var reason;\n \n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // continue with close in either case\n reason = `Received close from peer with Error Code ${errorCode}`;\n if (this.role === role.target)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason='requested', immediate=false)\n {\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} immediate=${immediate} reason:`, reason);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully(reason) {\n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n this.sender.enqueue(closeMessage)\n await new Promise(r => setImmediateN(r, 30));\n this.messageLedger.fulfillMessagePromise(closeMessage.id, {});\n }\n }\n\n /** sends close message to peer but doesn't require response \n */\n async closeImmediately(reason) {\n if(this.sender.inFlight)\n this.sender.clearFlightDeck(this.sender.inFlight.message, this.sender.nonce);\n \n let closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n closeMessage.nonce = this.sender.nonce;\n let signedCloseMessage = await closeMessage.sign();\n\n /* Overwrite the in-flight message because we don't care to deliver pending messages */\n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n debugging('connection') && console.debug(this.debugLabel, 'sending close message to peer');\n\n try\n {\n this.transport.send(signedCloseMessage);\n }\n catch(error)\n {\n debugging('connection') && console.debug(this.debugLabel, 'failed to send close message to ${this.loggableDest}:', error);\n }\n }\n \n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n {\n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n //\n // This implies that no API functions which call doClose may await between\n // their invocation and their call to doClose!\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.in(['closing', 'close-wait']));\n if (preCloseState === 'established' && !immediate) {\n try {\n if (immediate) {\n await this.closeImmediately(reason);\n } else {\n await this.closeGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.role === role.initiator)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (peer: ${this.peerAddress.address} dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1014');\n }\n \n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n \n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n } catch(error) {\n debugging() && console.warn(this.debugLabel, `could not close connection; dcpsid=${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\n this.state.set(['closing', 'close-wait'], 'closed');\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect() via this.sender.enqueue().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args)\n {\n if (!this.state.is('connected'))\n await this.connect();\n\n if (this.state.in(['closing', 'close-wait', 'closed']))\n throw new DCPError(`Connection (${this._id}) is ${this.state}; cannot send. (${this.loggableDest})`, 'DCPC-1001');\n\n const message = this.messageFactory.buildMessage(...args);\n return this.sender.enqueue(message);\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.hasNtp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function a$isFriendlyUrl(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform))\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, { family: 4 });\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family !== 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume tha all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = (__webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation)();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.debug(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.debug('scheduler config location is always secure for debug builds');\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n\n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.role === role.initiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection.js?");
4521
+ eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @author Wes Garland\n * @date January 2020, Feb 2021, Mar 2022\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established disconnected close-wait closing closed\n * =====================================================================================================================================\n * |-- i:connect ---->\n * |-- t:newTarget -->\n * X--------------------------------------------------------------------------------> doClose()\n * |-- transportDisconnectHandler -------------------------->\n * |-- i:connect ---------->\n * |-- t:establishTarget -->\n * |-- transportDisconnectHandler -->\n * <-- reconnect -------------------|\n * X--------- doClose() ------->\n * X- doClose() ->\n * XXX------------|---------------------|--------------|-----------------------------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n * \n * Error Codes relevant to DCP Connections:\n * DCPC-1001 - CONNECTION CANNOT SEND WHEN CLOSED\n * DCPC-1002 - MESSAGE CAME FROM INVALID SENDER\n * DCPC-1003 - MESSAGE SIGNATURE INVALID \n * DCPC-1004 - TRYING TO CONNECT AFTER ALREADY CONNECTED\n * DCPC-1005 - TRYING TO ESTABLISH TARGET AFTER TARGET ALREADY ESTABLISHED\n * DCPC-1006 - CONNECTION COULD NOT BE ESTABLISHED WITHIN 30 SECONDS\n * DCPC-1007 - RECEIVED MESSAGE PAYLOAD BEFORE CONNECT OPERATION\n * DCPC-1008 - TARGET RESPONDED WITH INVALID DCPSID\n * DCPC-1009 - MESSAGE IS OF UNKNOWN TYPE\n * DCPC-1010 - DUPLICATE TRANSMISSION RECEIPT\n * DCPC-1011 - DEFAULT ERROR CODE WHEN PEER SENDS CLOSE MESSAGE\n * DCPC-1012 - MESSAGE IS OF TYPE 'UNHANDLED MESSAGE'\n * DCPC-1013 - MESSAGE IS INVALID\n * DCPC-1014 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF ERROR\n */\n\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./connection-message */ \"./src/protocol-v4/connection/connection-message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nconst isDebugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting', /* initiator: establish first transport instance connection; target: listening */\n 'established',\n 'disconnected', /* connection is still valid, but underlying transport is no longer connected */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.1.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '^5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n var _role;\n \n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n\n if (target)\n _role = role.initiator;\n else\n _role = role.target;\n \n super(`Protocol Connection (${role})`);\n this.role = _role;\n \n if (target) {\n this.debugLabel = 'connection(i):';\n this._target = target;\n this.hasNtp = false;\n } else {\n this.debugLabel = 'connection(t):';\n this.hasNtp = true;\n }\n\n if (idKeystore) {\n this.identityPromise = Promise.resolve(idKeystore);\n } else {\n /* Always resolved by the time a session is established */\n debugging('connection') && console.debug('loading identity from wallet');\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n });\n\n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n\n this.receiver = new Receiver(this, this.messageLedger);\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n /* Create a connection config as this.connectionOptions which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n *\n * Having this as an exposed method instead of hidden in the constructor\n * is due to the lazy determination of the connection url.\n */\n this.configureConnectionForUrl = (url) => {\n this.url = url;\n this.connectionOptions = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'allowBatch': true,\n 'maxMessagesPerBatch': 100,\n 'identityUnlockTimeout': 300,\n 'ttl': {\n 'min': 15,\n 'max': 600,\n 'default': 120\n },\n 'transports': [ 'socketio' ],\n }),\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.role === role.initiator ? this.url.href : 'target'],\n connectionOptions\n );\n \n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 3000 : 20000); /** XXXwg make this configurable */\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n this.loggableDest = this.role === role.initiator ? this.url : '<target>';\n }\n\n /* By default, unsent messages cause .send() to reject for DCP intiators, but not targets. When\n * messages are unsent but not rejected, the send promise resolves with an instance of Error.\n *\n * Note: \"unsent messages\" are messages we tried to send, but couldn't be verified as sent because \n * the connection closed. It is plausible that they reached the other end, but also plausible that\n * they did not.\n *\n * @note XXX this is expedient, but not really correct. DCP is supposed to be completely peer-to-peer;\n * what we need to do is unify around one way or the other of handling unsent messages (probably\n * rejection), but -- importantly -- the daemons need to cross their Ts and dot their Is when it\n * comes to handling this stuff. My current thinking is that we could should use a specific DCP\n * error code for that, and make it non-fatal at the unhandledRejection when it's for a response,\n * but not a command.\n */\n this.rejectUnsentMessages = this.role === role.initiator;\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n *\n * @note this API is wrong. It should be using DCP Config fragments instead of (url,ks) /wg Mar 2022\n */\n static async newTarget(url, ks, transport) {\n const pk = await ks.getPrivateKey();\n const ksUnlocked = await new wallet.Keystore(pk, '') /* needed for daemon operation */\n const target = new Connection(undefined, ksUnlocked); \n\n assert(target.role === role.target);\n target._target = { location: url };\n target.transport = transport;\n\n await target.doPreConnectTasks();\n\n target.state.set('initial', 'connecting'); /* connecting => listen */\n return target;\n }\n\n /**\n * Non-API function which is mostly a design wart. This needs to be invoked\n * - after we know the connection URL\n * - before we make a message\n * - in an async way because there is a DNS lookup\n */\n async doPreConnectTasks()\n {\n if (!this.state.is('connecting') || this.sender)\n return;\n\n if (this.role === role.initiator && this._target.hasOwnProperty('friendLocation') && await a$isFriendlyUrl(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n \n this.sender = new Sender(this); // create sender before promises so that we can still enqueue messages before hopping off the event loop\n }\n \n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect()\n {\n if (this.state.is('initial'))\n {\n this.connectPromise = this.a$_connect();\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n this.connectPromise = this.a$_reconnect();\n return this.connectPromise;\n }\n \n if (this.state.is('connecting'))\n {\n assert(this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('established'))\n return;\n \n if (this.state.in(['closed', 'close-wait', 'closing']))\n throw new Error('connection already closed', 'DCPC-1016');\n\n throw new Error('impossible');\n }\n\n /**\n * Performs a reconnection for connections which are in the disconnected state, and\n * tries to send any in-flight or enqueued messages as soon as that happens.\n */\n async a$_reconnect()\n {\n assert(this.state.is('disconnected'));\n\n this.state.set('disconnected', 'connecting');\n debugging() && console.log(`391: entering a$connectToTarget...`);\n const connected = await this.a$connectToTarget();\n\n // If we didn't connect / bailed early, that suggests we collided with\n // another reconnect handler, so this attempt can be abandoned\n if (connected === false) {\n debugging('connection') && console.log(this.debugLabel, `396: Aborted extra reconnection attempt`);\n return;\n }\n\n this.state.set('connecting', 'established');\n \n debugging('connection') && console.log(this.debugLabel, `402: Reconnected`);\n\n this.emit('connect'); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n\n async a$_connect() {\n var presharedPeerAddress;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n\n // This has to happen after updating the state, or we get races due to \n // \"test->async->act on test result\" races\n await this.doPreConnectTasks();\n\n await this.a$connectToTarget();\n const establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session over ${this.transport.name}:`, error);\n this.close(error, true);\n throw error;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n let identity = await this._target.identity;\n\n if ( false\n || typeof identity !== 'object'\n || typeof identity.address !== 'object'\n || !(identity.address instanceof wallet.Address))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug(this.debugLabel, 'Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n\n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.notifyTransportReady();\n }\n\n /**\n * unreference any objects entrained by this connection so that it does not prevent\n * the node program from exiting naturally.\n */\n unref()\n {\n if (this.connectAbortTimer && this.connectAbortTimer.unref)\n this.connectAbortTimer.unref();\n }\n\n /**\n * Method is invoked when the transport disconnects. Transport instance is responsible for its own\n * finalization; Connection instance is responsible for finding a new transport, resuming the\n * connection, and retransmitting any in-flight message.\n */\n transportDisconnectHandler()\n {\n try\n { \n if (this.state.in(['disconnected', 'closing', 'close-wait', 'closed'])) /* transports may fire this more than once */\n return;\n \n this.state.set(['connecting', 'established'], 'disconnected');\n this.emit('disconnect'); /* UI hint: \"internet unavailable\" */\n debugging('connection') && console.debug(this.debugLabel, `Transport disconnected from ${this.url}; ${this.sender.inFlight ? 'have' : 'no'} in-flight message`);\n\n if (!this.dcpsid)\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\n \n if (this.role === role.target)\n {\n /* targets generally can't reconnect due to NAT */\n debugging('connection') && console.debug(this.debugLabel, `Waiting for initiator to reconnect for ${this.dcpsid}`);\n return;\n }\n\n if (!this.sender.inFlight && this.connectionOptions.onDemand)\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n else\n this.connect();\n }\n catch(error)\n {\n debugging('connection') && console.debug(error);\n this.close(error, true);\n\n if (error.code !== 'DCPC-1016')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target\n * - Rejects when we give up on all transports.\n * - Resolves when we have connected to target using a transport.\n *\n * The connection attempt will keep a node program \"alive\" while it is happening.\n * The `autoUnref` connectionOption and unref() methods offer ways to make this not\n * happen.\n */\n async a$connectToTarget()\n {\n const that = this;\n const availableTransports = [].concat(this.connectionOptions.transports);\n var quitMsg = false; /* not falsey => reject asap, value is error message */\n var quitCode = undefined;\n var boSleepIntr; /* if not falsey, a function that interrupts the backoff sleep */\n var transportConnectIntr; /* if not falsey, a function that interrupts the current connection attempt */\n\n // If there is already a connectAbortTimer, then we should signal the caller\n // that we were called in error\n if (this.connectAbortTimer)\n return false;\n\n /* This timer has the lifetime of the entire connection attempt. When we time out,\n * we set the quitMsg to get the retry loop to quit, then we interrupt the timer so\n * that we don't have to wait for the current backoff to expire before we notice, and\n * we expire the current attempt to connect right away as well.\n */\n this.connectAbortTimer = setTimeout(() => {\n quitMsg = 'connection timeout';\n if (boSleepIntr) boSleepIntr();\n if (transportConnectIntr) transportConnectIntr();\n }, this.connectionOptions.connectTimeout * 1000);\n\n if (this.connectionOptions.autoUnref)\n this.unref();\n\n /* cleanup code called on return/throw */\n function cleanup_ctt()\n {\n clearTimeout(that.connectAbortTimer);\n delete that.connectAbortTimer;\n }\n\n /* Connect to target with a specific transport. Resolves with { bool success, obj transport } */\n function a$connectWithTransport(transportName)\n { \n transportConnectIntr = false;\n\n return new Promise((connectWithTransport_resolve, connectWithTransport_reject) => { \n const TransportClass = Transport.require(transportName);\n const transport = new TransportClass(that.url, Object.assign({ connectionId: that.id }, that.connectionOptions[transportName]));\n var ret = { transport };\n\n function cleanup_cwt()\n {\n for (let eventName of transport.eventNames())\n for (let listener of transport.listeners(eventName))\n transport.off(eventName, listener);\n }\n \n /* In the case where we have a race condition in the transport implementation, arrange things\n * so that we resolve with whatever fired last if we have a double-fire on the same pass of \n * the event loop.\n */\n transport.on('connect', () => { cleanup_cwt(); ret.success=true; connectWithTransport_resolve(ret) });\n transport.on('error', (error) => { cleanup_cwt(); connectWithTransport_reject(error) });\n transport.on('connect-failed', (error) => {\n cleanup_cwt();\n ret.success = false;\n ret.error = error;\n debugging() && console.log(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close(true) };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1015'));\n }\n \n /* Loop while trying each available transport in turn. Sleep with exponential backoff between runs */\n while (!quitMsg)\n {\n for (let transportName of availableTransports)\n {\n try\n {\n const { success, error, transport } = await a$connectWithTransport(transportName);\n \n if (success === true)\n { /* have successfully connected to target */\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler());\n transport.on('close', () => this.transportDisconnectHandler());\n\n transportConnectIntr = false;\n cleanup_ctt();\n\n this.transport = transport;\n transport.peerVersion = this.peerVersion;\n // a connect event will be emitted in the caller, as well as a\n // call to this.sender.notifyTransportReady();\n \n return true; \n }\n\n if (error && error.httpStatus)\n {\n switch(error.httpStatus)\n {\n case 301: case 302: case 303: case 307: case 308:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n break;\n case 400: case 403: case 404:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n default:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; will try again with ${transportName}`);\n break;\n }\n }\n }\n catch (impossbleError)\n {\n /* transport connection attempts should never throw. */\n debugging('connection') && console.debug(this.debugLabel, `Error connecting to ${this.url} with ${transportName}; won't try again:`, impossibleError);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n }\n }\n \n if (availableTransports.length === 0)\n {\n quitMsg = 'all transports exhausted';\n break;\n }\n \n /* Go to (interruptible) sleep for a while before trying again */\n const backoffTimeMs = this.backoffTimeIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'trying again in', Number(backoffTimeMs / 1000).toFixed(2), 'seconds');\n const boSleepPromise = a$sleepMs(backoffTimeMs);\n boSleepIntr = boSleepPromise.intr;\n await boSleepPromise;\n boSleepIntr = false;\n } \n\n /* The only way we get here is for us to discover that the connection is unconnectable - eg \n * reject timer has expired or similar.\n */\n cleanup_ctt();\n throw new DCPError(quitMsg, 'DCPC-1016', quitCode);\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n assert(this.role === role.target);\n \n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid; \n this.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n *\n * XXXwg this code needs an audit re error handling: what message error should we be emitting?\n * why do we keep working after we find an error?\n *\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n var messageError;\n var messageValid = true;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n\n try\n {\n message = typeof messageJSON === 'object' ? messageJSON : JSON.parse(messageJSON);\n }\n catch(error)\n {\n console.error('connection::handleMessage received unparseable message from peer:', error);\n this.emit('error', error);\n return;\n }\n \n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, `duplicate message nonce=${message.body.nonce}:`, message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `received message ${message.body.type} ${message.body.id}; nonce=`, message.body.nonce);\n\n /* Capture the initial identity of the remote end during the connect operation */\n if (this.authorizedSender === null)\n {\n let messageBody = message.body;\n let payload = messageBody.payload;\n \n if (payload && message.body.type === 'batch')\n {\n for (let i=0; i < payload.length; i++)\n {\n let innerMessageBody = payload[i];\n\n if (innerMessageBody.payload && innerMessageBody.payload.operation === 'connect' && (innerMessageBody.type === 'response' || innerMessageBody.type === 'request'))\n {\n messageBody = innerMessageBody;\n payload = innerMessageBody.payload;\n break;\n }\n }\n }\n\n if (payload)\n {\n if (payload.operation === 'connect' && (messageBody.type === 'response' || messageBody.type === 'request'))\n this.authorizedSender = message.owner;\n else\n throw new DCPError('Message payload received before connection operation', 'DCPC-1007');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n messageError = new DCPError('Message came from invalid sender.', 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message owner was not an authorized sender - aborting connection');\n this.close(messageError, true);\n this.emit('error', messageError);\n return;\n }\n }\n\n if (this.role === role.target && this.state.in(['connecting']))\n {\n await this.doPreConnectTasks();\n\n // while connecting, the target gets its nonce from the initiator\n this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message signature: ${validation.errorMessage}`, 'DCPC-1003');\n debugging('connection') && console.debug(this.debugLabel, 'Message signature failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;\n }\n\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n messageError = new DCPError(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`,'DCPC-1012');\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(messageError, true);\n messageValid = false;\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1013');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;;\n }\n\n if (!messageValid) {\n message.body.type = 'unhandled-message'\n this.emit('error', messageError);\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, `acking message. ${message.body.id}; token=`, message.body.ackToken);\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n try {\n this.transport.send(signedMessage);\n }\n catch(error) {\n console.warn(this.debugLabel, `Failed to ack message ${message.id}: `, error.message);\n };\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateSignature(message)\n {\n if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\n \n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n\n if (!signatureValid)\n {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessage(message)\n {\n try\n {\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message signature does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce)\n {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' }; // eslint-disable-line no-unreachable\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command\n */\n closeWait (errorCode = null)\n {\n const preCloseState = this.state.valueOf();\n var reason;\n \n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // continue with close in either case\n reason = `Received close from peer with Error Code ${errorCode}`;\n if (this.role === role.target)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason='requested', immediate=false)\n {\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} immediate=${immediate} reason:`, reason);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully(reason) {\n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n this.sender.enqueue(closeMessage)\n await new Promise(r => setImmediateN(r, 30));\n this.messageLedger.fulfillMessagePromise(closeMessage.id, {});\n }\n }\n\n /** sends close message to peer but doesn't require response \n */\n async closeImmediately(reason) {\n if(this.sender.inFlight)\n this.sender.clearFlightDeck(this.sender.inFlight.message, this.sender.nonce);\n \n let closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n closeMessage.nonce = this.sender.nonce;\n let signedCloseMessage = await closeMessage.sign();\n\n /* Overwrite the in-flight message because we don't care to deliver pending messages */\n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n debugging('connection') && console.debug(this.debugLabel, 'sending close message to peer');\n\n try\n {\n this.transport.send(signedCloseMessage);\n }\n catch(error)\n {\n debugging('connection') && console.debug(this.debugLabel, 'failed to send close message to ${this.loggableDest}:', error);\n }\n }\n \n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n {\n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n //\n // This implies that no API functions which call doClose may await between\n // their invocation and their call to doClose!\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.in(['closing', 'close-wait']));\n if (preCloseState === 'established' && !immediate) {\n try {\n if (immediate) {\n await this.closeImmediately(reason);\n } else {\n await this.closeGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.role === role.initiator)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (peer: ${this.peerAddress.address} dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1014');\n }\n \n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n \n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n } catch(error) {\n debugging() && console.warn(this.debugLabel, `could not close connection; dcpsid=${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\n this.state.set(['closing', 'close-wait'], 'closed');\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect() via this.sender.enqueue().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args)\n {\n if (!this.state.is('connected'))\n await this.connect();\n\n if (this.state.in(['closing', 'close-wait', 'closed']))\n throw new DCPError(`Connection (${this._id}) is ${this.state}; cannot send. (${this.loggableDest})`, 'DCPC-1001');\n\n const message = this.messageFactory.buildMessage(...args);\n return this.sender.enqueue(message);\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.hasNtp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function a$isFriendlyUrl(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform))\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, { family: 4 });\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family !== 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume tha all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = (__webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation)();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.debug(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.debug('scheduler config location is always secure for debug builds');\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n\n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.role === role.initiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection.js?");
4522
4522
 
4523
4523
  /***/ }),
4524
4524
 
@@ -4583,7 +4583,7 @@ eval("/**\n * @file protocol/connection/message-ledger.js\n * @author
4583
4583
  \************************************************/
4584
4584
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4585
4585
 
4586
- eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The receiver class is responsible for listening on a transport instance\n * for messages, and for parsing and validating messages as they are received.\n * Upon a valid message, it will do the following:\n * On Request: emit on the 'request' event\n * On Response: resolve the request receipt that corresponds to the message ID\n */\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nlet nanoid;\nlet perf;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n perf = performance;\n}\n\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { validityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\n\nclass Receiver {\n constructor(connection, messageLedger) {\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `receiver(i#${id}):` : `receiver(t#${id}):`;\n\n // Used by non-ntp connections to determine \"the present\"\n // time: seconds since epoch when response was sent\n // receivedMs: monotonically increasing timestamp of receipt in milliseconds\n this.lastResponseTiming = {\n time: Date.now() / 1000,\n receivedMs: perf.now(),\n }\n }\n\n /**\n * This method validates the ttl period on a request.\n * @param {Connection.Request} req\n * @returns {boolean} true if the ttl is valid, false otherwise\n */\n isTtlValid(req) {\n const validity = req.payload.validity;\n if (!validity || !validity.stamp || typeof validity.time !== 'number') {\n const err = new DCPError(\n 'Invalid request. Validity property must have `time` and `stamp`.',\n 'EINVAL',\n );\n req.respond(err);\n return false;\n } else {\n // determine ttl of the request\n let ttl;\n if (!validity.ttl || typeof validity.ttl !== 'number') {\n ttl = this.connection.connectionOptions.ttl.default || this.connection.connectionOptions.ttl.min;\n } else {\n if (validity > this.connection.connectionOptions.ttl.max) ttl = this.connection.connectionOptions.ttl.max;\n else if (validity.ttl < this.connection.connectionOptions.ttl.min) ttl = this.connection.connectionOptions.ttl.min;\n else ttl = validity.ttl;\n }\n\n const now = this.connection.currentTime();\n const minimumPresent = now - dcpConfig.dcp.validitySlopValue;\n const maximumPresent = now + dcpConfig.dcp.validitySlopValue;\n const expirationTime = validity.time + ttl;\n\n if (validity.time > maximumPresent) {\n const err = new DCPError(\n `Request was sent from the future (${validity.time} > ${maximumPresent})`,\n 'ETIMETRAVEL',\n );\n req.respond(err);\n return false;\n } else if (expirationTime < minimumPresent) {\n const err = new DCPError(\n `Request validity has expired (${expirationTime} < ${minimumPresent})`,\n 'EEXPIRED',\n {\n timestamp: validity.time,\n offset: now - validity.time,\n },\n );\n req.respond(err);\n return false;\n }\n\n if (validityStampCache.insert(validity.stamp, expirationTime) !== true)\n {\n const err = new DCPError(\n `Duplicate request validity stamp: ${validity.stamp}`,\n 'EDUP',\n );\n req.respond(err);\n return false;\n }\n }\n\n return true;\n }\n\n messageBodyDebugLogger(body, batchInfo)\n {\n const dcpsid = this.connection.dcpsid || '<new>';\n\n function now()\n {\n const d = new Date();\n var ms = (d % 1000) + '';\n var ds;\n\n switch (ms.length)\n {\n case 1:\n ms = '00' + ms;\n break;\n case 2:\n ms = '0' + ms;\n break;\n }\n \n ds = d.toLocaleTimeString().replace(/ .*/, '.' + ms);\n return ds;\n }\n \n if (body.type === 'response') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent response`, (body.payload && body.payload.operation) || '');\n } else if (body.type === 'request') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent request ${body.payload && body.payload.operation}`);\n } else if (body.type === 'batch') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent batch of length ${body.payload.length}, ${body.id.slice(-6)}`);\n } else {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent invalid message body`, body);\n }\n }\n\n /**\n * Central implementation for dispatching events based on message bodies.\n */\n dispatchMessageBody(messageBody, owner, batchDebugLabel)\n {\n debugging('messages') && this.messageBodyDebugLogger(messageBody, batchDebugLabel);\n \n if (messageBody.type === 'response') {\n this.onResponse(messageBody, owner);\n } else if (messageBody.type === 'request') {\n this.onRequest(messageBody, owner);\n } else if (messageBody.type === 'batch') {\n this.onBatch(messageBody, owner);\n } else if (messageBody.type === 'unhandled-message') {\n debugging('receiver') && console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'DCPC-1009');\n }\n }\n \n /**\n * Central method for receiver to begin processing a message.\n * This method trusts that the message has a valid dcpsid, signature,\n * and nonce.\n * @param {object} message validated message object\n */\n handleMessage(message) {\n const owner = new wallet.Address(message.owner);\n\n this.dispatchMessageBody(message.body, owner, '');\n }\n\n /**\n * This method is invoked when the connection receives a new request.\n * It should validate the request against the session state, and handle\n * special-case session operations like 'connect' and 'close'.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onRequest(msgBody, owner) {\n const req = this.connection.Request.fromJSON(msgBody, owner);\n\n if (this.connection.state.isNot('established')) { /* XXXwg narrow down this list */\n if (req.payload.operation !== 'connect') {\n const message = \"First request operation received from peer was not 'connect'\";\n debugging('receiver') && console.error(this.debugLabel, message);\n this.connection.close(message, true);\n return\n }\n this.handleFirstRequest(req).catch( (err) => {\n console.error(this.debugLabel, `bad first request was sent to target. ${err.code || '(no code)'}`, err, err.code);\n this.connection.close(err, true);\n });\n } else if (!this.isTtlValid(req)) {\n /** XXXwg regardless the cause, we should not be ignoring requests. Spec says we need to send back *something* */\n ( true) && console.debug(this.debugLabel, 'Request has invalid TTL; ignoring request. Validity=', req.payload.validity);\n return;\n } else {\n let reqId = req.id;\n\n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* FIXME XXXwg this is completely insane */\n /* req.id frequently mutates between memoization and timeout */\n delete this.connection.openRequests[reqId];\n }, 1000);\n this.handleOperation(req);\n }\n }\n\n async handleFirstRequest(req) {\n const cacheSize = 10; /* 0 should be enough */\n var cache; /* we keep a cache of recently-seen half sid in case sender retransmits a connect message */\n var bottomHalfSid;\n \n debugging('receiver') && console.debug(this.debugLabel, 'received first request.');\n if (!this.handleFirstRequest.cache)\n this.handleFirstRequest.cache = {};\n cache = this.handleFirstRequest.cache;\n \n const initiatorVersion = req.payload.data.version;\n const targetCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(initiatorVersion, targetCompatibility);\n if (!versionCompatible) {\n const versionErr = new DCPError(targetCompatibility, 'DCPC-EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n const topHalfSid = req.payload.data.sid;\n\n if (cache.hasOwnProperty(topHalfSid)) {\n bottomHalfSid = cache[topHalfSid];\n console.error(`*** Warning: remote presented recently-seen half sid ${topHalfSid}; reusing ${bottomHalfSid}`);\n } else {\n bottomHalfSid = nanoid();\n cache[topHalfSid] = bottomHalfSid;\n if (Object.keys(cache).length > cacheSize) {\n for (let prop in cache) {\n delete cache[prop];\n break;\n }\n }\n }\n\n if (typeof topHalfSid !== 'string' || topHalfSid.length < bottomHalfSid.length) {\n throw new DCPError(`Initiator sent an invalid DCPSID: ${topHalfSid}`, 'DCPC-EDCPSID');\n }\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n version: this.connection.constructor.VERSION,\n operation: 'connect'\n });\n\n // we must emit `established` before sending this response to ensure\n // that when the initiator sends its first request we are in that state.\n this.connection.establishTarget(dcpsid, req.owner);\n try {\n await this.connection.sender.specialFirstSend(connectResponse);\n } catch (error) {\n console.error('Failed to send `connect` response.');\n return;\n }\n\n // usually established, but it's possible to close before this fires.\n debugging('receiver') && console.debug(this.debugLabel, 'first request handled. Connection state:', this.connection.state.valueOf());\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'DCPC-ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.payload.errorCode).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond().catch(error => {\n if (error.code === 'DCPC-1011')\n return;\n throw error;\n });\n break;\n\n default:\n this.connection.emit('request', req);\n break;\n }\n }\n\n /**\n * This method is invoked when the connection receives a new response.\n * It should validate the response against the session state.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onResponse(msgBody, owner) {\n debugging('receiver') && console.debug(this.debugLabel, 'received response.');\n const resp = this.connection.Response.fromJSON(msgBody, owner);\n\n this.lastResponseTiming.time = resp.time;\n this.lastResponseTiming.receivedMs = perf.now();\n\n this.messageLedger.fulfillMessagePromise(resp.id, resp);\n }\n\n /**\n * This method handles rehydrating Batch Requests and then\n * passes internal messages on to appropriate handlers.\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onBatch(msgBody, owner) {\n const batch = this.connection.Batch.fromJSON(msgBody, owner);\n var batchDebugLabel;\n \n debugging() && (batchDebugLabel = ' batch ' + batch.id.slice(-6));\n\n for (let messageBody of batch.messageObjects)\n this.dispatchMessageBody(messageBody, owner, batchDebugLabel);\n }\n}\n\nObject.assign(module.exports, {\n Receiver,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/receiver.js?");
4586
+ eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The receiver class is responsible for listening on a transport instance\n * for messages, and for parsing and validating messages as they are received.\n * Upon a valid message, it will do the following:\n * On Request: emit on the 'request' event\n * On Response: resolve the request receipt that corresponds to the message ID\n */\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nlet nanoid;\nlet perf;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n perf = performance;\n}\n\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { validityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\n\nclass Receiver {\n constructor(connection, messageLedger) {\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `receiver(i#${id}):` : `receiver(t#${id}):`;\n\n // Used by non-ntp connections to determine \"the present\"\n // time: seconds since epoch when response was sent\n // receivedMs: monotonically increasing timestamp of receipt in milliseconds\n this.lastResponseTiming = {\n time: Date.now() / 1000,\n receivedMs: perf.now(),\n }\n }\n\n /**\n * This method validates the ttl period on a request.\n * @param {Connection.Request} req\n * @returns {boolean} true if the ttl is valid, false otherwise\n */\n isTtlValid(req) {\n const validity = req.payload.validity;\n if (!validity || !validity.stamp || typeof validity.time !== 'number') {\n const err = new DCPError(\n 'Invalid request. Validity property must have `time` and `stamp`.',\n 'EINVAL',\n );\n req.respond(err);\n return false;\n } else {\n // determine ttl of the request\n let ttl;\n if (!validity.ttl || typeof validity.ttl !== 'number') {\n ttl = this.connection.connectionOptions.ttl.default || this.connection.connectionOptions.ttl.min;\n } else {\n if (validity > this.connection.connectionOptions.ttl.max) ttl = this.connection.connectionOptions.ttl.max;\n else if (validity.ttl < this.connection.connectionOptions.ttl.min) ttl = this.connection.connectionOptions.ttl.min;\n else ttl = validity.ttl;\n }\n\n const now = this.connection.currentTime();\n const minimumPresent = now - dcpConfig.dcp.validitySlopValue;\n const maximumPresent = now + dcpConfig.dcp.validitySlopValue;\n const expirationTime = validity.time + ttl;\n\n if (validity.time > maximumPresent) {\n const err = new DCPError(\n `Request was sent from the future (${validity.time} > ${maximumPresent})`,\n 'ETIMETRAVEL',\n );\n req.respond(err);\n return false;\n } else if (expirationTime < minimumPresent) {\n const err = new DCPError(\n `Request validity has expired (${expirationTime} < ${minimumPresent})`,\n 'EEXPIRED',\n {\n timestamp: validity.time,\n offset: now - validity.time,\n },\n );\n req.respond(err);\n return false;\n }\n\n if (validityStampCache.insert(validity.stamp, expirationTime) !== true)\n {\n const err = new DCPError(\n `Duplicate request validity stamp: ${validity.stamp}`,\n 'EDUP',\n );\n req.respond(err);\n return false;\n }\n }\n\n return true;\n }\n\n messageBodyDebugLogger(body, batchInfo)\n {\n const dcpsid = this.connection.dcpsid || '<new>';\n\n function now()\n {\n const d = new Date();\n var ms = (d % 1000) + '';\n var ds;\n\n switch (ms.length)\n {\n case 1:\n ms = '00' + ms;\n break;\n case 2:\n ms = '0' + ms;\n break;\n }\n \n ds = d.toLocaleTimeString().replace(/ .*/, '.' + ms);\n return ds;\n }\n \n if (body.type === 'response') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent response`, (body.payload && body.payload.operation) || '');\n } else if (body.type === 'request') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent request ${body.payload && body.payload.operation}`);\n } else if (body.type === 'batch') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent batch of length ${body.payload.length}, ${body.id.slice(-6)}`);\n } else {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent invalid message body`, body);\n }\n }\n\n /**\n * Central implementation for dispatching events based on message bodies.\n */\n dispatchMessageBody(messageBody, owner, batchDebugLabel)\n {\n debugging('messages') && this.messageBodyDebugLogger(messageBody, batchDebugLabel);\n \n if (messageBody.type === 'response') {\n this.onResponse(messageBody, owner);\n } else if (messageBody.type === 'request') {\n this.onRequest(messageBody, owner);\n } else if (messageBody.type === 'batch') {\n this.onBatch(messageBody, owner);\n } else if (messageBody.type === 'unhandled-message') {\n debugging('receiver') && console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'DCPC-1009');\n }\n }\n \n /**\n * Central method for receiver to begin processing a message.\n * This method trusts that the message has a valid dcpsid, signature,\n * and nonce.\n * @param {object} message validated message object\n */\n handleMessage(message) {\n const owner = new wallet.Address(message.owner);\n\n this.dispatchMessageBody(message.body, owner, '');\n }\n\n /**\n * This method is invoked when the connection receives a new request.\n * It should validate the request against the session state, and handle\n * special-case session operations like 'connect' and 'close'.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onRequest(msgBody, owner) {\n const req = this.connection.Request.fromJSON(msgBody, owner);\n\n if (this.connection.state.isNot('established')) { /* XXXwg narrow down this list */\n if (req.payload.operation !== 'connect') {\n const message = \"First request operation received from peer was not 'connect'\";\n debugging('receiver') && console.error(this.debugLabel, message);\n this.connection.close(message, true);\n return\n }\n this.handleFirstRequest(req).catch( (err) => {\n console.error(this.debugLabel, `bad first request was sent to target. ${err.code || '(no code)'}`, err, err.code);\n this.connection.close(err, true);\n });\n } else if (!this.isTtlValid(req)) {\n /** XXXwg regardless the cause, we should not be ignoring requests. Spec says we need to send back *something* */\n ( true) && console.debug(this.debugLabel, 'Request has invalid TTL; ignoring request. Validity=', req.payload.validity);\n return;\n } else {\n let reqId = req.id;\n\n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* FIXME XXXwg this is completely insane */\n /* req.id frequently mutates between memoization and timeout */\n delete this.connection.openRequests[reqId];\n }, 1000);\n this.handleOperation(req);\n }\n }\n\n async handleFirstRequest(req) {\n const cacheSize = 10; /* 0 should be enough */\n var cache; /* we keep a cache of recently-seen half sid in case sender retransmits a connect message */\n var bottomHalfSid;\n \n debugging('receiver') && console.debug(this.debugLabel, 'received first request.');\n if (!this.handleFirstRequest.cache)\n this.handleFirstRequest.cache = {};\n cache = this.handleFirstRequest.cache;\n \n const initiatorVersion = req.payload.data.version;\n const targetCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(initiatorVersion, targetCompatibility);\n if (!versionCompatible) {\n debugging('receiver') && console.debug(this.debugLabel, `Initiator version ${initiatorVersion} does not meet target targetCompatibility '${targetCompatibility}'`);\n const versionErr = new DCPError(`Initiator version ${initiatorVersion} does not meet target targetCompatibility '${targetCompatibility}'`, 'DCPC-EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = initiatorVersion;\n\n const topHalfSid = req.payload.data.sid;\n\n if (cache.hasOwnProperty(topHalfSid)) {\n bottomHalfSid = cache[topHalfSid];\n console.error(`*** Warning: remote presented recently-seen half sid ${topHalfSid}; reusing ${bottomHalfSid}`);\n } else {\n bottomHalfSid = nanoid();\n cache[topHalfSid] = bottomHalfSid;\n if (Object.keys(cache).length > cacheSize) {\n for (let prop in cache) {\n delete cache[prop];\n break;\n }\n }\n }\n\n if (typeof topHalfSid !== 'string' || topHalfSid.length < bottomHalfSid.length) {\n throw new DCPError(`Initiator sent an invalid DCPSID: ${topHalfSid}`, 'DCPC-EDCPSID');\n }\n\n // Play silly buggers with the protocol version, so 5.0.0 clients will still\n // respect us, but more recent clients will behave better\n const version = (this.connection.peerVersion === '5.0.0')\n ? '5.0.0' // how do you do, fellow 5.0.0\n : this.connection.constructor.VERSION;\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n version,\n operation: 'connect'\n });\n\n // we must emit `established` before sending this response to ensure\n // that when the initiator sends its first request we are in that state.\n this.connection.establishTarget(dcpsid, req.owner);\n try {\n await this.connection.sender.specialFirstSend(connectResponse);\n } catch (error) {\n console.error('Failed to send `connect` response.');\n return;\n }\n\n // usually established, but it's possible to close before this fires.\n debugging('receiver') && console.debug(this.debugLabel, 'first request handled. Connection state:', this.connection.state.valueOf());\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'DCPC-ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.payload.errorCode).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond().catch(error => {\n if (error.code === 'DCPC-1011')\n return;\n throw error;\n });\n break;\n\n default:\n this.connection.emit('request', req);\n break;\n }\n }\n\n /**\n * This method is invoked when the connection receives a new response.\n * It should validate the response against the session state.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onResponse(msgBody, owner) {\n debugging('receiver') && console.debug(this.debugLabel, 'received response.');\n const resp = this.connection.Response.fromJSON(msgBody, owner);\n\n this.lastResponseTiming.time = resp.time;\n this.lastResponseTiming.receivedMs = perf.now();\n\n this.messageLedger.fulfillMessagePromise(resp.id, resp);\n }\n\n /**\n * This method handles rehydrating Batch Requests and then\n * passes internal messages on to appropriate handlers.\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onBatch(msgBody, owner) {\n const batch = this.connection.Batch.fromJSON(msgBody, owner);\n var batchDebugLabel;\n \n debugging() && (batchDebugLabel = ' batch ' + batch.id.slice(-6));\n\n for (let messageBody of batch.messageObjects)\n this.dispatchMessageBody(messageBody, owner, batchDebugLabel);\n }\n}\n\nObject.assign(module.exports, {\n Receiver,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/receiver.js?");
4587
4587
 
4588
4588
  /***/ }),
4589
4589
 
@@ -4616,7 +4616,7 @@ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan
4616
4616
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4617
4617
 
4618
4618
  "use strict";
4619
- eval("/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `sender(i#${id}):` : `sender(t#${id}):`;\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection._id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection._id}-connect-${nanoid()}`;\n debugging('sender') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('sender') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n\n /**\n * Invoked when the connection class becomes aware that a transport is available upon which\n * we can deliver traffic, this method either sends the current in-flight message (from a \n * previous transport instance) or services the queue.\n */\n notifyTransportReady()\n {\n debugging('sender') && console.debug(this.debugLabel, `Notified transport is ready. inflight ${!!this.inFlight}, state ${this.connection.state}`)\n if (this.inFlight)\n this.sendInFlightMessage();\n else\n this.connection.keepalive(); // This will pump the message queue\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSend([message]); /* XXXwg - messageLedger may leak messages when createAndSend throws */\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise} from messageLedger\n * @resolves to a response if sending a request, or resolves when the response was sent.\n */\n enqueue(message) {\n /* Note - it looks like message.type is always undefined in here, but that somehow \n * the message is mutated between enqueue and transmission. /wg Nov 2021\n */\n debugging('sender') && console.debug(this.debugLabel, 'enqueueing message', message.id);\n debugging('sender') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, 'message:', message);\n this.queue.push(message);\n \n if (this.connection.state.is('initial'))\n this.connection.connect();\n else if (this.connection.state.is('disconnected'))\n this.connection.connect();\n\n setImmediate(() => this.serviceQueue());\n return this.messageLedger.addMessage(message);\n }\n\n /**\n * Creates a batch and puts it in flight if the connection is established \n * and no batch is already in flight.\n */\n async serviceQueue () {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait'])) {\n debugging('sender') && console.debug(this.debugLabel, `servicing queue. for ${this.connection.state} inFlight=${!!this.inFlight} connection ${this.connection.loggableDest}`);\n this.createAndSend();\n } else {\n debugging('sender') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Creates a batch if it can find any messages to send and sends them.\n * If there are no messages to send, simply returns.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n *\n * @returns a Promise which resolves to a number which represents the number of messages sent. This is \n * either 0, 1, or the number of messages in a batch (possibly 1).\n */\n async createAndSend(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('sender') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0)\n {\n debugging('sender') && console.debug(this.debugLabel, 'createAndSend: nothing to send');\n return 0;\n }\n assert(!this.inFlight);\n \n let op;\n let opIsReq = false;\n if (messages.length === 1) { // ie. no need to batch, can send single message\n op = messages[0];\n op.ackToken = this.makeAckToken();\n opIsReq = true;\n } else {\n op = new this.connection.Batch(messages, this.makeAckToken());\n debugging('sender') && console.debug(this.debugLabel, `created a batch of length ${messages.length}`);\n }\n\n op.nonce = this.nonce;\n this.inFlight = { message: op };\n let signedOp = await op.sign();\n this.inFlight.signedMessage = signedOp;\n if (!opIsReq)\n this.messageLedger.addBatch(op);\n\n this.connection.emit('send', this.inFlight);\n this.sendInFlightMessage();\n return messages.length;\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * `clearFlightDeck` is the only method that should be resetting this.inFlight\n * and thus closing the loop.\n */\n sendInFlightMessage()\n {\n assert(this.inFlight);\n\n debugging('sender') && console.debug(this.debugLabel, `sending in-flight message ${this.inFlight.message.id}`);\n\n try\n {\n const op = this.inFlight.message;\n let type;\n \n /** XXXwg todo - figure out why Request/Response have name=Message and get rid of tests */\n if (op instanceof this.connection.Request)\n type = 'Request';\n else if (op instanceof this.connection.Response)\n type = 'Response';\n else \n type = op.constructor.name;\n \n debugging('sender') && console.debug(this.debugLabel, `sending ${type}`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n this.connection.transport.send(this.inFlight.signedMessage);\n }\n catch (error)\n {\n console.error(`Error while sending message ${this.inFlight.message.id} to ${this.connection.loggableDest}:`, error);\n debugging('sender') && console.debug(this.debugLabel, 'call stack:', new Error().stack);\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n if (this.inFlight !== null) {\n debugging('sender') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n this.serviceQueue();\n }\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('sender') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get batchSize() {\n if (this._batchSize) return this._batchSize;\n const batchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n return this._batchSize;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/sender.js?");
4619
+ eval("/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `sender(i#${id}):` : `sender(t#${id}):`;\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection._id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection._id}-connect-${nanoid()}`;\n debugging('sender') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n debugging('sender') && console.log(this.debugLabel, `Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`);\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = targetVersion;\n if (this.connection.transport)\n this.connection.transport.peerVersion = targetVersion;\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('sender') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n\n /**\n * Invoked when the connection class becomes aware that a transport is available upon which\n * we can deliver traffic, this method either sends the current in-flight message (from a \n * previous transport instance) or services the queue.\n */\n notifyTransportReady()\n {\n debugging('sender') && console.debug(this.debugLabel, `Notified transport is ready. inflight ${!!this.inFlight}, state ${this.connection.state}`)\n if (this.inFlight)\n this.sendInFlightMessage();\n else\n this.connection.keepalive(); // This will pump the message queue\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSend([message]); /* XXXwg - messageLedger may leak messages when createAndSend throws */\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise} from messageLedger\n * @resolves to a response if sending a request, or resolves when the response was sent.\n */\n enqueue(message) {\n /* Note - it looks like message.type is always undefined in here, but that somehow \n * the message is mutated between enqueue and transmission. /wg Nov 2021\n */\n debugging('sender') && console.debug(this.debugLabel, 'enqueueing message', message.id);\n debugging('sender') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, 'message:', message);\n this.queue.push(message);\n \n if (this.connection.state.is('initial'))\n this.connection.connect();\n else if (this.connection.state.is('disconnected'))\n this.connection.connect();\n\n setImmediate(() => this.serviceQueue());\n return this.messageLedger.addMessage(message);\n }\n\n /**\n * Creates a batch and puts it in flight if the connection is established \n * and no batch is already in flight.\n */\n async serviceQueue () {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait'])) {\n debugging('sender') && console.debug(this.debugLabel, `servicing queue. for ${this.connection.state} inFlight=${!!this.inFlight} connection ${this.connection.loggableDest}`);\n this.createAndSend();\n } else {\n debugging('sender') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Creates a batch if it can find any messages to send and sends them.\n * If there are no messages to send, simply returns.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n *\n * @returns a Promise which resolves to a number which represents the number of messages sent. This is \n * either 0, 1, or the number of messages in a batch (possibly 1).\n */\n async createAndSend(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('sender') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0)\n {\n debugging('sender') && console.debug(this.debugLabel, 'createAndSend: nothing to send');\n return 0;\n }\n assert(!this.inFlight);\n \n let op;\n let opIsReq = false;\n if (messages.length === 1) { // ie. no need to batch, can send single message\n op = messages[0];\n op.ackToken = this.makeAckToken();\n opIsReq = true;\n } else {\n op = new this.connection.Batch(messages, this.makeAckToken());\n debugging('sender') && console.debug(this.debugLabel, `created a batch of length ${messages.length}`);\n }\n\n op.nonce = this.nonce;\n this.inFlight = { message: op };\n let signedOp = await op.sign();\n this.inFlight.signedMessage = signedOp;\n if (!opIsReq)\n this.messageLedger.addBatch(op);\n\n this.connection.emit('send', this.inFlight);\n this.sendInFlightMessage();\n return messages.length;\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * `clearFlightDeck` is the only method that should be resetting this.inFlight\n * and thus closing the loop.\n */\n sendInFlightMessage()\n {\n assert(this.inFlight);\n\n debugging('sender') && console.debug(this.debugLabel, `sending in-flight message ${this.inFlight.message.id}`);\n\n try\n {\n const op = this.inFlight.message;\n let type;\n \n /** XXXwg todo - figure out why Request/Response have name=Message and get rid of tests */\n if (op instanceof this.connection.Request)\n type = 'Request';\n else if (op instanceof this.connection.Response)\n type = 'Response';\n else \n type = op.constructor.name;\n \n debugging('sender') && console.debug(this.debugLabel, `sending ${type}`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n this.connection.transport.send(this.inFlight.signedMessage);\n }\n catch (error)\n {\n console.error(`Error while sending message ${this.inFlight.message.id} to ${this.connection.loggableDest}:`, error);\n debugging('sender') && console.debug(this.debugLabel, 'call stack:', new Error().stack);\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n if (this.inFlight !== null) {\n debugging('sender') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n this.serviceQueue();\n }\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('sender') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get batchSize() {\n if (this._batchSize) return this._batchSize;\n const batchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n return this._batchSize;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/sender.js?");
4620
4620
 
4621
4621
  /***/ }),
4622
4622
 
@@ -4670,7 +4670,7 @@ eval("/**\n * @file protocol/transport/index.js\n * @author Ryan Ros
4670
4670
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4671
4671
 
4672
4672
  "use strict";
4673
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file transport/socketio.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @author Wes Garland, wes@kingsds.network \n * @date January 2020, March 2022\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n *\n * Transport can operate in either ack mode or non-ack-mode, depending on the value of this.ackMode.\n * Ack mode sends an extra socketio-layer packet back after each message is received, and theoretically\n * this can be used meter or measure bandwidth or potentially second-guess socketio's ability to\n * interleave its heartbeat messages OOB from the main traffic. This mode could potentially be enabled\n * on-demand as well, although it appears that it is not necessary to get the desired behaviour at this\n * point, so the feature is off-by-default (although reasonably well-tested).\n */\n\n\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst socketioClient = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\");\n\nclass SocketIOTransport extends Transport\n{\n /**\n * @constructor\n * @param {socket} [networkHnd] Target: an instance of socket.io socket that represents\n * a new connection.\n */\n /**\n * @constructor create new instance of a socketio-flavoured Transport.\n *\n * @param {Url} url Initiator: the URL of the target we want to connect to.\n * Target: the socket to adopt for communication\n *\n * @param {object} options The `socketio` property of a protocol-v4 connectionOptions \n * object, which was built from a combination of defaults and \n * various dcpConfig components.\n *\n * @param {Server} [networkHnd] Target: an instance of httpServer (or Express app??) that\n * we want to listen on.\n *\n * @returns instance suitable for listening or sending (depending on mode), but does not manage connections.\n */\n constructor(url, options={}, networkHnd)\n {\n super('Protocol SocketIO Transport');\n\n assert(DcpURL.isURL(url));\n assert(typeof options === 'object');\n \n this.name = 'socketio';\n this.url = url;\n this.connSequence = 0;\n this.msgSequence = 0;\n this.isClosed = false; /* true => connection finalization begun */\n this.isFinal = false; /* true => connection finalization completed */\n this.hasConnected = false; /* true => one 'connect' event has been processed */\n this.rxFragments = {}; /* messages we are receiving in fragments */\n this.rxPending = []; /* strings which are messages that are complete and ready to emit, or objects that in this.rxFragments */\n this.txPending = []; /* messages we are receiving in fragments */\n this.txNumInFlight = 0; /* number of txPending sent but not acknowleged */\n this.ackMode = false; /* true => enable ack-mode */\n this.debugInfo = { remoteLabel: '<unknown>' };\n\n this.options = leafMerge(\n /* Defaults */\n ({\n autoUnref: dcpEnv.platform === 'nodejs' ? true : undefined, /* socket.io + webpack 5 { node:global } work-around */\n perMessageDeflate: dcpConfig.build !== 'debug',\n maxFragmentCount: 1000, /* used to limit memory on receiver */\n maxFragmentSize: 1e6, /* bytes */\n maxHttpBufferSize: 10 * 1e6, /* bytes */\n pingTimeout: 30 * 1e3, /* s */\n pingInterval: 90 * 1e3, /* s */\n transports: ['polling', 'websocket'],\n upgrade: true,\n rememberUpgrade: true,\n autoConnect: true,\n cors: {\n origin: '*',\n methods: ['GET', 'POST']\n },\n }),\n options,\n /* Absolutely mandatory */\n ({\n path: this.url.pathname\n }));\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n this.options.maxHttpBufferSize /= 10;\n this.options.maxFragmentSize /= 10;\n this.options.maxFragmentCount *= 10;\n \n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n this.options.pingTimeout /= 5;\n this.options.pingInterval /= 5;\n }\n }\n \n if (arguments.length === 2) /* Initiator */\n {\n this.url = url;\n this.debugLabel = 'socketio(i):';\n this.socket = socketioClient.connect(url.origin, this.options);\n this.debugInfo.remoteLabel = url.href;\n\n this.socket.on('connect_error', (error) => !this.isClosed && this.handleConnectErrorEvent(error));\n this.socket.on('connect', () => !this.isClosed && this.handleConnectEvent());\n }\n else /* Target */\n {\n const socketioServer = dcpEnv.platform === 'nodejs' && requireNative('socket.io');\n\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('target mode only supported in nodejs');\n\n if (options.connectionId)\n {\n this.debugLabel = `socketio(t:${options.connectionId})`\n this.connectionId = options.connectionId;\n delete this.options.connectionId;\n }\n else\n this.debugLabel = 'socketio(t):';\n\n if (socketioServer && networkHnd instanceof socketioServer.Socket)\n {\n /* Target subordinate - receives socketio instance from this.handleConnectionEvent */\n this.socket = networkHnd;\n debugging('socketio') && console.debug(`New socket on ${this.socket.handshake && this.socket.handshake.url}, ${this.socket.id}`);\n this.debugInfo.remoteLabel = networkHnd.handshake.address.replace(/^::ffff:/,'');\n }\n else\n {\n /* Target top-level - receives http server from connection manager */\n const httpServer = networkHnd;\n assert(typeof httpServer === 'object', typeof httpServer.listen === 'function');\n\n this.socket = socketioServer(httpServer, this.options);\n this.socket.on('connection', (socket) => !this.isClosed && this.handleConnectionEvent(socket));\n debugging() && console.debug('Listening on', this.url);\n this.once('close', () => httpServer.close());\n }\n }\n \n /* Ensure config won't hang concatenation code */\n const fragmentOverhead = 500000; /* Should really be around 65K but testing suggests this is needed. Why?? /wg Mar 2022 */\n if (this.options.maxFragmentSize + fragmentOverhead > this.options.maxHttpBufferSize)\n this.options.maxFragmentSize = this.options.maxHttpBufferSize - fragmentOverhead;\n if (!(this.options.maxFragmentSize > 1000)) /* let's at least fill most of an ethernet frame */\n this.options.maxFragmentSize = 1000;\n if (this.options.maxFragmentSize + fragmentOverhead > this.options.maxHttpBufferSize)\n throw new DCPError('invalid http buffer or fragment size', 'DCP-1106');\n\n this.socket.compress(dcpConfig.build !== 'debug'); /* try to keep traffic sniffable in debug */\n\n this.socket.on('message', (msg, syncCallback) => !this.isFinal && this.handleMessageEvent(msg, syncCallback));\n this.socket.on('disconnect', (reason) => !this.isClosed && this.handleDisconnectEvent(reason));\n\n if (dcpConfig.build === 'debug' || debugging('socketio'))\n {\n this.socket.on('ping', (data) => debugging('socketio') && console.debug(this.debugLabel, 'received ping'));\n this.socket.on('pong', (data) => debugging('socketio') && console.debug(this.debugLabel, 'received pong'));\n }\n }\n\n /** \n * socket the socket.io disconnect event, which is fired upon disconnection.\n * For some reasons (explicit disconnection), the socket.io code will not try to reconnect, but\n * in all other cases, the socket.io client will normally wait for a small random delay and then \n * try to reconnect, but in our case, we simply tear the transport down and like the Connection\n * class handle reconnects, since it might prefer a different transport at this point anyhow.\n *\n * One tricky case here is ping timeout, since the ping timeout includes the time to transmit the\n * packet before it was sent.\n * \n * @param {string} reason Possible reasons for disconnection.\n */\n handleDisconnectEvent(reason)\n {\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.debugInfo.remoteLabel}; reason=${reason}`);\n\n if (this.isClosed !== true)\n setImmediate(() => this.close());\n\n switch(reason)\n {\n case 'io client disconnect':\n return; /* we called socket.disconnect() from another \"thread\" */\n case 'io server disconnect':\n case 'ping timeout':\n case 'transport close':\n case 'transport error':\n this.emit('end', reason);\n /* fallthrough */\n default:\n }\n }\n\n /**\n * Handle a socketio message from the peer.\n *\n * Most of the time, a socketio message contains a DCP message, however we support other message\n * types as well, with a mechanism inspired by 3GPP TS 23.040 (GSM 03.40) message concatenation.\n */\n handleMessageEvent(msg, syncCallback)\n {\n if (typeof syncCallback === 'function') \n syncCallback(true);\n\n if (typeof msg !== 'string')\n {\n debugging('socketio') && console.debug(this.debugLabel, `received ${typeof msg} message from peer`, this.debugInfo.remoteLabel);\n return;\n }\n\n switch (msg[0])\n {\n case '{':\n this.processMessage(msg);\n break;\n case 'E':\n this.processExtendedMessage(msg);\n break;\n case 'A':\n this.processAcknowledgment(msg);\n break;\n default:\n throw new DCPError(this.debugLabel, `Unrecognized message type indicator from ${this.debugInfo.remoteLabel} (${msg.charCodeAt(0)})`, 'DCPC-1102');\n }\n }\n\n /**\n * Process an ordinary message. This just a hunk of JSON that gets passed up to the connection.\n */\n processMessage(msg)\n {\n this.rxPending.push(msg);\n this.emitReadyMessages();\n if (this.ackMode)\n this.socket.send('A0000-ACK'); /* Ack w/o header = normal message */\n }\n\n /**\n * Remote has acknowledged the receipt of a message fragment. When in ack mode, this\n * triggers us to send more messages from the txPending queue. We always ack event when \n * not in ack-mode. See processExtendedMessage() comment for header description.\n */\n processAcknowledgment(msg)\n {\n this.txNumInFlight--;\n this.drainTxPending();\n\n /* Future: this is where maxInFlight might be tuned, based on how fast we get here */\n assert(this.txNumInFlight >= 0);\n }\n \n /**\n * Extended messages have the following format:\n * Octet(s) Description\n * 0 E (69) or A (65)\n * 1..4 Header size (N) in hexadecimal\n * 5 Header Type\n * C (67) => concatenation\n * 6..6 + N: Header\n *\n * Upon receipt, extended messages are acknowledged by retransmitting their header, but\n * with the first character changed from E to A.\n */\n processExtendedMessage(msg)\n {\n const headerSize = parseInt(msg.slice(1,5), 16);\n const headerType = msg[5];\n const header = msg.slice(6, 6 + headerSize);\n\n switch(headerType)\n {\n case 'C':\n this.processExtendedMessage_concatenated(header, msg.slice(6 + headerSize));\n break;\n default:\n throw new DCPError(`Unrecognized extended message header type indicator (${msg.charCodeAt(5)})`, 'DCPC-1101');\n }\n\n if (this.ackMode)\n this.socket.send('A' + msg.slice(1, 6 + headerSize));\n }\n\n /**\n * We have received a fragment of a concatenation extended message. Memoize the fragment, and\n * if this is the last fragment, transform the fragments list into a pending message string and\n * then emits all ready messages (pending message strings).\n *\n * This code is capable of handling messages whose fragments arrive out of order. Complete messages \n * are emitted in the order that their first parts are received. Socketio is supposed to be total\n * order-aware but I wanted a safety belt for intermingled messages without occupying the entire\n * socketio network buffer on the first pass of the event loop. Because the first message fragment\n * is emitted to socketio on the same event loop pass as their Connection::send calls, I believe\n * that are no cases where messages will be emitted out of order at the other end, even when \n * intermingled on the wire, and the Nth message is much longer than any (N+k)th messages.\n */\n processExtendedMessage_concatenated(header, payload)\n {\n const that = this;\n\n function panic(error)\n {\n debugging('socketio') && console.debug(error);\n that.emit('error', error);\n that.close(true, error.message);\n }\n\n try\n {\n header = JSON.parse(header);\n }\n catch(e)\n {\n throw new DCPError(`invalid extended message header '${header}'`, 'DCPC-1103');\n }\n \n if (!(header.total <= this.options.maxFragmentCount))\n return panic(new DCPError('excessive message fragmentation', 'DCPC-1107')); /* make it more difficult for attacker to force OOM us */\n\n if (!(header.seq < header.total))\n return panic(new DCPError(`corrupt header for part ${header.seq + 1} of ${header.total} of message ${header.msgId}`, 'DCPC-1108'));\n \n /* First fragment received, initial data structures and memoize the message's arrival order via pending list */\n if (!this.rxFragments[header.msgId])\n {\n const fragments = [];\n this.rxFragments[header.msgId] = fragments;\n this.rxPending.push(fragments);\n }\n\n this.rxFragments[header.msgId][header.seq] = payload;\n debugging('socketio') && console.debug(this.debugLabel, 'received part', header.seq + 1, 'of', header.total, 'for message', header.msgId);\n\n if (header.total !== this.rxFragments[header.msgId].filter(el => el !== undefined).length)\n return;\n\n debugging('socketio') && console.debug(this.debugLabel, 'received all parts of message', header.msgId);\n \n const idx = this.rxPending.indexOf(this.rxFragments[header.msgId])\n this.rxPending[idx] = this.rxFragments[header.msgId].join('');\n delete this.rxFragments[header.msgId];\n this.emitReadyMessages();\n }\n\n /**\n * Emit message events so that the connection can process them. The pending property is an\n * array which is used to order messages, so that they are emitted in the order they were\n * sent. There are edge cases in the concatenation code where a sender could theoretically\n * interleave multiple messages, and have a second, shorter, message fully received before \n * the first is fully transmitted.\n *\n * The pending property stores only strings or objects; objects are effectively positional\n * memoes which are turned into strings when they are ready to be emitted to the connection\n * for processing.\n */\n emitReadyMessages()\n {\n while (this.rxPending.length && typeof this.rxPending[0] === 'string')\n this.emit('message', this.rxPending.shift());\n }\n \n /** \n * Close the current instance of Socket.\n *\n * This function effectively finalizes the socket so that it can't be used any more and\n * (hopefully) does not entrain garbage. A 'close' event is emitted at the end of socket\n * finalization and should be hooked by things that might entrain this transport instance to free\n * up memory, like maybe a list of active transports, etc.\n *\n * We add few extra hops on/off the event loop here to try and clean up any messages halfway in/out\n * the door that we might want to process. That happens between the setting of the isClosed and \n * isFinal flags.\n *\n * @param {boolean} immediate\n * @param {string} reason\n */\n close(immediate, reason)\n {\n const that = this;\n debugging('socketio') && console.debug(this.debugLabel, 'closing connection' + (reason ? ` (${reason})` : ''));\n\n function finalize(socket)\n {\n if (that.isFinal)\n return;\n\n socket.removeAllListeners();\n that.isFinal = true;\n\n /* Free up memory that might be huge in case something entrains us */\n that.rxPending.length = 0;\n that.txPending.length = 0;\n for (let msgId in that.rxFragments)\n that.rxFragments[msgId].length = 0;\n }\n\n function closeSocket()\n {\n const socket = that.socket;\n if (!socket)\n return;\n\n try\n {\n that.socket.disconnect();\n that.isClosed = true;\n that.emit('close', reason);\n delete that.socket;\n }\n finally\n {\n if (immediate)\n finalize(socket);\n else\n setImmediateN(() => finalize(socket), 3);\n }\n }\n\n if (!this.isClosed)\n {\n if (immediate)\n closeSocket();\n else\n setImmediateN(closeSocket, 3);\n }\n }\n\n /**\n * Handle the socketio connect_error event, which is fired when:\n * - the low-level connection cannot be established\n * - the connection is denied by the server in a middleware function\n *\n * In the first case, socket.io will automatically try to reconnect, after a delay,\n * except that will cancel that reconnection attempt here by killing the transport\n * instance so that the Connection class can try failing over to an alternate transport.\n *\n * @param {Error} error optional instance of error describing the underlying reason for the\n * connect failure. If the underlying reason is expressible by an\n * HTTP status code, this code will be placed as a Number in the\n * description property.\n */\n handleConnectErrorEvent(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, `unable to connect to ${this.url}`, error);\n if (error.type === 'TransportError' && typeof error.description === 'number')\n error.httpStatus = Number(error.description);\n\n this.emit('connect-failed', error);\n this.close(true, error.message);\n }\n\n /** \n * Handle the socketio connect event, which is fired by the Socket instance upon connection \n * and reconnection.\n */\n handleConnectEvent()\n {\n if (this.hasConnected === true)\n debugging('socketio') && console.debug(this.debugLabel, `*** reconnected to ${this.debugInfo.remoteLabel} (should be impossible)`);\n else\n {\n /* initial connection */\n this.hasConnected = true;\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', this.debugInfo.remoteLabel);\n this.emit('connect');\n }\n }\n\n /**\n * Handle the socketio connection event, which is fired upon a connection from client.\n * Used by target:top-level to create target:subordinate transport instances.\n */\n handleConnectionEvent(socket)\n {\n var options = { connectionId: (this.options.connectionId ? this.options.connectionId + '.' : '') + ++this.connSequence };\n var subordinate = new SocketIOTransport(this.url, options, socket);\n\n this.emit('connection', subordinate);\n }\n\n /**\n * Send a message to the transport instance on the other side of this connection.\n *\n * @param {string} message the message to send\n */\n send(message)\n {\n const msgSequence = ++this.msgSequence;\n\n debugging('socketio') && console.debug(this.debugLabel, `sending message ${msgSequence} to`, this.debugInfo.remoteLabel);\n debugging('socketio') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, message);\n\n if (!this.socket)\n throw new DCPError('SocketIOTransport.send: Not connected', 'DCPC-1105');\n \n if (!(message.length > this.options.maxFragmentSize))\n this.txPending.push({ message, msgSequence });\n else\n { /* This is a large message. Use message concatenation to send in fragments. A failure of any\n * fragment is a failure of the entire message and will be handled at the connection layer.\n */\n let total = Math.ceil(message.length / this.options.maxFragmentSize);\n \n for (let seq=0; seq < total; seq++)\n {\n let start = seq * this.options.maxFragmentSize;\n let fragment = message.slice(start, start + this.options.maxFragmentSize);\n let header = JSON.stringify({ msgId: msgSequence, seq, total });\n let extMessage = 'E' + header.length.toString(16).padStart(4, '0') + 'C' + header + fragment;\n\n this.txPending.push({ message: extMessage, msgSequence, seq, total });\n\n /* This should be impossible, but it also effectively kills the connection. */\n if (header.length > 0xFFFF)\n throw new DCPError('extended message header too long', 'DCPC-1104');\n }\n }\n\n this.drainTxPending();\n }\n\n /**\n * Drain messages from the txPending queue out to socketio's network buffer. This drain is controlled\n * by acknowledgment messages so that socketio can have the opportunity to insert ping/pong messages\n * in the data stream.\n *\n * The reason we put non-concat messages through here is to avoid special casing a bunch of nasty\n * things for the sake of a little tiny bit of performance. Sending message<-ACK and queueing through\n * the drain code is necessary if we wind up interleaving multiple message sends in some interesting\n * edge cases; notably, fast-teardown, but a future Connection class that ran without nonces (or multiple\n * nonces) would need this, too.\n *\n * non-ack-mode is compatible with previous dcp5 versions and probably a little faster\n */\n async drainTxPending()\n {\n const that = this;\n const maxInFlight = 2; /* need to verify bandwidth against ping time before increasing, so not tuneable at this time */\n debugging('socketio') && console.debug(this.debugLabel, 'drain tx pending queue' + (this.ackMode ? `; ${maxInFlight - this.txNumInFlight} slots available` : ''));\n \n for (let i=0;\n !this.isClosed && this.txPending.length && (this.txNumInFlight < maxInFlight || !this.ackMode);\n i++)\n {\n const pendingElement = this.txPending.shift();\n await new Promise(resolve => writeToSocket(pendingElement, resolve));\n\n if (this.ackMode)\n this.txNumInFlight++;\n }\n\n function writeToSocket(pendingElement, syncCallback)\n {\n if (that.isClosed)\n return;\n\n const { message, msgSequence, seq, total } = pendingElement;\n if (typeof syncCallback === 'function')\n that.socket.send(message, syncCallback);\n else\n that.socket.send(message);\n\n if (typeof seq !== 'undefined')\n debugging('socketio') && console.debug(that.debugLabel, `sent fragment ${seq + 1}/${total} of message ${msgSequence} (${message.length} bytes)`);\n else\n debugging('socketio') && console.debug(that.debugLabel, `sent message ${msgSequence} (${message.length} bytes)`);\n }\n }\n}\n\nexports.TransportClass = SocketIOTransport;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/socketio.js?");
4673
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file transport/socketio.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @author Wes Garland, wes@kingsds.network \n * @date January 2020, March 2022\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n *\n * Transport can operate in either ack mode or non-ack-mode, depending on the value of this.ackMode.\n * Ack mode sends an extra socketio-layer packet back after each message is received, and theoretically\n * this can be used meter or measure bandwidth or potentially second-guess socketio's ability to\n * interleave its heartbeat messages OOB from the main traffic. This mode could potentially be enabled\n * on-demand as well, although it appears that it is not necessary to get the desired behaviour at this\n * point, so the feature is off-by-default (although reasonably well-tested).\n */\n\n\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst socketioClient = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\n\nclass SocketIOTransport extends Transport\n{\n /**\n * @constructor\n * @param {socket} [networkHnd] Target: an instance of socket.io socket that represents\n * a new connection.\n */\n /**\n * @constructor create new instance of a socketio-flavoured Transport.\n *\n * @param {Url} url Initiator: the URL of the target we want to connect to.\n * Target: the socket to adopt for communication\n *\n * @param {object} options The `socketio` property of a protocol-v4 connectionOptions \n * object, which was built from a combination of defaults and \n * various dcpConfig components.\n *\n * @param {Server} [networkHnd] Target: an instance of httpServer (or Express app??) that\n * we want to listen on.\n *\n * @returns instance suitable for listening or sending (depending on mode), but does not manage connections.\n */\n constructor(url, options={}, networkHnd)\n {\n super('Protocol SocketIO Transport');\n\n assert(DcpURL.isURL(url));\n assert(typeof options === 'object');\n \n this.name = 'socketio';\n this.url = url;\n this.connSequence = 0;\n this.msgSequence = 0;\n this.isClosed = false; /* true => connection finalization begun */\n this.isFinal = false; /* true => connection finalization completed */\n this.hasConnected = false; /* true => one 'connect' event has been processed */\n this.rxFragments = {}; /* messages we are receiving in fragments */\n this.rxPending = []; /* strings which are messages that are complete and ready to emit, or objects that in this.rxFragments */\n this.txPending = []; /* messages we are receiving in fragments */\n this.txNumInFlight = 0; /* number of txPending sent but not acknowleged */\n this.ackMode = false; /* true => enable ack-mode */\n this.debugInfo = { remoteLabel: '<unknown>' };\n\n this.options = leafMerge(\n /* Defaults */\n ({\n autoUnref: dcpEnv.platform === 'nodejs' ? true : undefined, /* socket.io + webpack 5 { node:global } work-around */\n perMessageDeflate: dcpConfig.build !== 'debug',\n maxFragmentCount: 1000, /* used to limit memory on receiver */\n maxFragmentSize: 1e6, /* bytes */\n maxHttpBufferSize: 10 * 1e6, /* bytes */\n pingTimeout: 30 * 1e3, /* s */\n pingInterval: 90 * 1e3, /* s */\n transports: ['polling', 'websocket'],\n upgrade: true,\n rememberUpgrade: true,\n autoConnect: true,\n cors: {\n origin: '*',\n methods: ['GET', 'POST']\n },\n }),\n options,\n /* Absolutely mandatory */\n ({\n path: this.url.pathname\n }));\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n this.options.maxHttpBufferSize /= 10;\n this.options.maxFragmentSize /= 10;\n this.options.maxFragmentCount *= 10;\n \n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n this.options.pingTimeout /= 5;\n this.options.pingInterval /= 5;\n }\n }\n \n if (arguments.length === 2) /* Initiator */\n {\n this.url = url;\n this.debugLabel = 'socketio(i):';\n this.socket = socketioClient.connect(url.origin, this.options);\n this.debugInfo.remoteLabel = url.href;\n\n this.socket.on('connect_error', (error) => !this.isClosed && this.handleConnectErrorEvent(error));\n this.socket.on('connect', () => !this.isClosed && this.handleConnectEvent());\n }\n else /* Target */\n {\n const socketioServer = dcpEnv.platform === 'nodejs' && requireNative('socket.io');\n\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('target mode only supported in nodejs');\n\n if (options.connectionId)\n {\n this.debugLabel = `socketio(t:${options.connectionId})`\n this.connectionId = options.connectionId;\n delete this.options.connectionId;\n }\n else\n this.debugLabel = 'socketio(t):';\n\n if (socketioServer && networkHnd instanceof socketioServer.Socket)\n {\n /* Target subordinate - receives socketio instance from this.handleConnectionEvent */\n this.socket = networkHnd;\n debugging('socketio') && console.debug(`New socket on ${this.socket.handshake && this.socket.handshake.url}, ${this.socket.id}`);\n this.debugInfo.remoteLabel = networkHnd.handshake.address.replace(/^::ffff:/,'');\n }\n else\n {\n /* Target top-level - receives http server from connection manager */\n const httpServer = networkHnd;\n assert(typeof httpServer === 'object', typeof httpServer.listen === 'function');\n\n this.socket = socketioServer(httpServer, this.options);\n this.socket.on('connection', (socket) => !this.isClosed && this.handleConnectionEvent(socket));\n debugging() && console.debug('Listening on', this.url);\n this.once('close', () => httpServer.close());\n }\n }\n \n /* Ensure config won't hang concatenation code */\n const fragmentOverhead = 500000; /* Should really be around 65K but testing suggests this is needed. Why?? /wg Mar 2022 */\n if (this.options.maxFragmentSize + fragmentOverhead > this.options.maxHttpBufferSize)\n this.options.maxFragmentSize = this.options.maxHttpBufferSize - fragmentOverhead;\n if (!(this.options.maxFragmentSize > 1000)) /* let's at least fill most of an ethernet frame */\n this.options.maxFragmentSize = 1000;\n if (this.options.maxFragmentSize + fragmentOverhead > this.options.maxHttpBufferSize)\n throw new DCPError('invalid http buffer or fragment size', 'DCP-1106');\n\n this.socket.compress(dcpConfig.build !== 'debug'); /* try to keep traffic sniffable in debug */\n\n this.socket.on('message', (msg, syncCallback) => !this.isFinal && this.handleMessageEvent(msg, syncCallback));\n this.socket.on('disconnect', (reason) => !this.isClosed && this.handleDisconnectEvent(reason));\n\n if (dcpConfig.build === 'debug' || debugging('socketio'))\n {\n this.socket.on('ping', (data) => debugging('socketio') && console.debug(this.debugLabel, 'received ping'));\n this.socket.on('pong', (data) => debugging('socketio') && console.debug(this.debugLabel, 'received pong'));\n }\n }\n\n /** \n * socket the socket.io disconnect event, which is fired upon disconnection.\n * For some reasons (explicit disconnection), the socket.io code will not try to reconnect, but\n * in all other cases, the socket.io client will normally wait for a small random delay and then \n * try to reconnect, but in our case, we simply tear the transport down and like the Connection\n * class handle reconnects, since it might prefer a different transport at this point anyhow.\n *\n * One tricky case here is ping timeout, since the ping timeout includes the time to transmit the\n * packet before it was sent.\n * \n * @param {string} reason Possible reasons for disconnection.\n */\n handleDisconnectEvent(reason)\n {\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.debugInfo.remoteLabel}; reason=${reason}`);\n\n if (this.isClosed !== true)\n setImmediate(() => this.close());\n\n switch(reason)\n {\n case 'io client disconnect':\n return; /* we called socket.disconnect() from another \"thread\" */\n case 'io server disconnect':\n case 'ping timeout':\n case 'transport close':\n case 'transport error':\n this.emit('end', reason);\n /* fallthrough */\n default:\n }\n }\n\n /**\n * Handle a socketio message from the peer.\n *\n * Most of the time, a socketio message contains a DCP message, however we support other message\n * types as well, with a mechanism inspired by 3GPP TS 23.040 (GSM 03.40) message concatenation.\n */\n handleMessageEvent(msg, syncCallback)\n {\n if (typeof syncCallback === 'function') \n syncCallback(true);\n\n if (typeof msg !== 'string')\n {\n debugging('socketio') && console.debug(this.debugLabel, `received ${typeof msg} message from peer`, this.debugInfo.remoteLabel);\n return;\n }\n\n switch (msg[0])\n {\n case '{':\n this.processMessage(msg);\n break;\n case 'E':\n this.processExtendedMessage(msg);\n break;\n case 'A':\n this.processAcknowledgment(msg);\n break;\n default:\n throw new DCPError(this.debugLabel, `Unrecognized message type indicator from ${this.debugInfo.remoteLabel} (${msg.charCodeAt(0)})`, 'DCPC-1102');\n }\n }\n\n /**\n * Process an ordinary message. This just a hunk of JSON that gets passed up to the connection.\n */\n processMessage(msg)\n {\n this.rxPending.push(msg);\n this.emitReadyMessages();\n if (this.ackMode)\n this.socket.send('A0000-ACK'); /* Ack w/o header = normal message */\n }\n\n /**\n * Remote has acknowledged the receipt of a message fragment. When in ack mode, this\n * triggers us to send more messages from the txPending queue. We always ack event when \n * not in ack-mode. See processExtendedMessage() comment for header description.\n */\n processAcknowledgment(msg)\n {\n this.txNumInFlight--;\n this.drainTxPending();\n\n /* Future: this is where maxInFlight might be tuned, based on how fast we get here */\n assert(this.txNumInFlight >= 0);\n }\n \n /**\n * Extended messages have the following format:\n * Octet(s) Description\n * 0 E (69) or A (65)\n * 1..4 Header size (N) in hexadecimal\n * 5 Header Type\n * C (67) => concatenation\n * 6..6 + N: Header\n *\n * Upon receipt, extended messages are acknowledged by retransmitting their header, but\n * with the first character changed from E to A.\n */\n processExtendedMessage(msg)\n {\n const headerSize = parseInt(msg.slice(1,5), 16);\n const headerType = msg[5];\n const header = msg.slice(6, 6 + headerSize);\n\n switch(headerType)\n {\n case 'C':\n this.processExtendedMessage_concatenated(header, msg.slice(6 + headerSize));\n break;\n default:\n throw new DCPError(`Unrecognized extended message header type indicator (${msg.charCodeAt(5)})`, 'DCPC-1101');\n }\n\n if (this.ackMode)\n this.socket.send('A' + msg.slice(1, 6 + headerSize));\n }\n\n /**\n * We have received a fragment of a concatenation extended message. Memoize the fragment, and\n * if this is the last fragment, transform the fragments list into a pending message string and\n * then emits all ready messages (pending message strings).\n *\n * This code is capable of handling messages whose fragments arrive out of order. Complete messages \n * are emitted in the order that their first parts are received. Socketio is supposed to be total\n * order-aware but I wanted a safety belt for intermingled messages without occupying the entire\n * socketio network buffer on the first pass of the event loop. Because the first message fragment\n * is emitted to socketio on the same event loop pass as their Connection::send calls, I believe\n * that are no cases where messages will be emitted out of order at the other end, even when \n * intermingled on the wire, and the Nth message is much longer than any (N+k)th messages.\n */\n processExtendedMessage_concatenated(header, payload)\n {\n const that = this;\n\n function panic(error)\n {\n debugging('socketio') && console.debug(error);\n that.emit('error', error);\n that.close(true, error.message);\n }\n\n try\n {\n header = JSON.parse(header);\n }\n catch(e)\n {\n throw new DCPError(`invalid extended message header '${header}'`, 'DCPC-1103');\n }\n \n if (!(header.total <= this.options.maxFragmentCount))\n return panic(new DCPError('excessive message fragmentation', 'DCPC-1107')); /* make it more difficult for attacker to force OOM us */\n\n if (!(header.seq < header.total))\n return panic(new DCPError(`corrupt header for part ${header.seq + 1} of ${header.total} of message ${header.msgId}`, 'DCPC-1108'));\n \n /* First fragment received, initial data structures and memoize the message's arrival order via pending list */\n if (!this.rxFragments[header.msgId])\n {\n const fragments = [];\n this.rxFragments[header.msgId] = fragments;\n this.rxPending.push(fragments);\n }\n\n this.rxFragments[header.msgId][header.seq] = payload;\n debugging('socketio') && console.debug(this.debugLabel, 'received part', header.seq + 1, 'of', header.total, 'for message', header.msgId);\n\n if (header.total !== this.rxFragments[header.msgId].filter(el => el !== undefined).length)\n return;\n\n debugging('socketio') && console.debug(this.debugLabel, 'received all parts of message', header.msgId);\n \n const idx = this.rxPending.indexOf(this.rxFragments[header.msgId])\n this.rxPending[idx] = this.rxFragments[header.msgId].join('');\n delete this.rxFragments[header.msgId];\n this.emitReadyMessages();\n }\n\n /**\n * Emit message events so that the connection can process them. The pending property is an\n * array which is used to order messages, so that they are emitted in the order they were\n * sent. There are edge cases in the concatenation code where a sender could theoretically\n * interleave multiple messages, and have a second, shorter, message fully received before \n * the first is fully transmitted.\n *\n * The pending property stores only strings or objects; objects are effectively positional\n * memoes which are turned into strings when they are ready to be emitted to the connection\n * for processing.\n */\n emitReadyMessages()\n {\n while (this.rxPending.length && typeof this.rxPending[0] === 'string')\n this.emit('message', this.rxPending.shift());\n }\n \n /** \n * Close the current instance of Socket.\n *\n * This function effectively finalizes the socket so that it can't be used any more and\n * (hopefully) does not entrain garbage. A 'close' event is emitted at the end of socket\n * finalization and should be hooked by things that might entrain this transport instance to free\n * up memory, like maybe a list of active transports, etc.\n *\n * We add few extra hops on/off the event loop here to try and clean up any messages halfway in/out\n * the door that we might want to process. That happens between the setting of the isClosed and \n * isFinal flags.\n *\n * @param {boolean} immediate\n * @param {string} reason\n */\n close(immediate, reason)\n {\n const that = this;\n debugging('socketio') && console.debug(this.debugLabel, 'closing connection' + (reason ? ` (${reason})` : ''));\n\n function finalize(socket)\n {\n if (that.isFinal)\n return;\n\n socket.removeAllListeners();\n that.isFinal = true;\n\n /* Free up memory that might be huge in case something entrains us */\n that.rxPending.length = 0;\n that.txPending.length = 0;\n for (let msgId in that.rxFragments)\n that.rxFragments[msgId].length = 0;\n }\n\n function closeSocket()\n {\n const socket = that.socket;\n if (!socket)\n return;\n\n try\n {\n that.socket.disconnect();\n that.isClosed = true;\n that.emit('close', reason);\n delete that.socket;\n }\n finally\n {\n if (immediate)\n finalize(socket);\n else\n setImmediateN(() => finalize(socket), 3);\n }\n }\n\n if (!this.isClosed)\n {\n if (immediate)\n closeSocket();\n else\n setImmediateN(closeSocket, 3);\n }\n }\n\n /**\n * Handle the socketio connect_error event, which is fired when:\n * - the low-level connection cannot be established\n * - the connection is denied by the server in a middleware function\n *\n * In the first case, socket.io will automatically try to reconnect, after a delay,\n * except that will cancel that reconnection attempt here by killing the transport\n * instance so that the Connection class can try failing over to an alternate transport.\n *\n * @param {Error} error optional instance of error describing the underlying reason for the\n * connect failure. If the underlying reason is expressible by an\n * HTTP status code, this code will be placed as a Number in the\n * description property.\n */\n handleConnectErrorEvent(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, `unable to connect to ${this.url}`, error);\n if (error.type === 'TransportError' && typeof error.description === 'number')\n error.httpStatus = Number(error.description);\n\n this.emit('connect-failed', error);\n this.close(true, error.message);\n }\n\n /** \n * Handle the socketio connect event, which is fired by the Socket instance upon connection \n * and reconnection.\n */\n handleConnectEvent()\n {\n if (this.hasConnected === true)\n debugging('socketio') && console.debug(this.debugLabel, `*** reconnected to ${this.debugInfo.remoteLabel} (should be impossible)`);\n else\n {\n /* initial connection */\n this.hasConnected = true;\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', this.debugInfo.remoteLabel);\n this.emit('connect');\n }\n }\n\n /**\n * Handle the socketio connection event, which is fired upon a connection from client.\n * Used by target:top-level to create target:subordinate transport instances.\n */\n handleConnectionEvent(socket)\n {\n var options = { connectionId: (this.options.connectionId ? this.options.connectionId + '.' : '') + ++this.connSequence };\n var subordinate = new SocketIOTransport(this.url, options, socket);\n\n this.emit('connection', subordinate);\n }\n\n /**\n * Send a message to the transport instance on the other side of this connection.\n *\n * @param {string} message the message to send\n */\n send(message)\n {\n const msgSequence = ++this.msgSequence;\n\n debugging('socketio') && console.debug(this.debugLabel, `sending message ${msgSequence} to`, this.debugInfo.remoteLabel);\n debugging('socketio') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, message);\n\n if (!this.socket)\n throw new DCPError('SocketIOTransport.send: Not connected', 'DCPC-1105');\n \n // iff message length is small enough for a fragment OR client is 5.0.0\n // send the message intact; big messages to modern clients get fragmented\n if (!(message.length > this.options.maxFragmentSize)\n || !(this.peerVersion && semver.satisfies(this.peerVersion, '>= 5.1.0')))\n this.txPending.push({ message, msgSequence });\n else\n { /* This is a large message. Use message concatenation to send in fragments. A failure of any\n * fragment is a failure of the entire message and will be handled at the connection layer.\n */\n let total = Math.ceil(message.length / this.options.maxFragmentSize);\n \n for (let seq=0; seq < total; seq++)\n {\n let start = seq * this.options.maxFragmentSize;\n let fragment = message.slice(start, start + this.options.maxFragmentSize);\n let header = JSON.stringify({ msgId: msgSequence, seq, total });\n let extMessage = 'E' + header.length.toString(16).padStart(4, '0') + 'C' + header + fragment;\n\n this.txPending.push({ message: extMessage, msgSequence, seq, total });\n\n /* This should be impossible, but it also effectively kills the connection. */\n if (header.length > 0xFFFF)\n throw new DCPError('extended message header too long', 'DCPC-1104');\n }\n }\n\n this.drainTxPending();\n }\n\n /**\n * Drain messages from the txPending queue out to socketio's network buffer. This drain is controlled\n * by acknowledgment messages so that socketio can have the opportunity to insert ping/pong messages\n * in the data stream.\n *\n * The reason we put non-concat messages through here is to avoid special casing a bunch of nasty\n * things for the sake of a little tiny bit of performance. Sending message<-ACK and queueing through\n * the drain code is necessary if we wind up interleaving multiple message sends in some interesting\n * edge cases; notably, fast-teardown, but a future Connection class that ran without nonces (or multiple\n * nonces) would need this, too.\n *\n * non-ack-mode is compatible with previous dcp5 versions and probably a little faster\n */\n async drainTxPending()\n {\n const that = this;\n const maxInFlight = 2; /* need to verify bandwidth against ping time before increasing, so not tuneable at this time */\n debugging('socketio') && console.debug(this.debugLabel, 'drain tx pending queue' + (this.ackMode ? `; ${maxInFlight - this.txNumInFlight} slots available` : ''));\n \n for (let i=0;\n !this.isClosed && this.txPending.length && (this.txNumInFlight < maxInFlight || !this.ackMode);\n i++)\n {\n const pendingElement = this.txPending.shift();\n // iff the peer is 5.1.x and will be sending a sync-acknowledgement,\n // we should wait for it; older peers should go hellbent for leather\n if (that.peerVersion && semver.satisfies(that.peerVersion, '>= 5.1.0'))\n await new Promise(resolve => writeToSocket(pendingElement, resolve));\n else\n writeToSocket(pendingElement);\n\n if (this.ackMode)\n this.txNumInFlight++;\n }\n\n function writeToSocket(pendingElement, syncCallback)\n {\n if (that.isClosed)\n return;\n\n const { message, msgSequence, seq, total } = pendingElement;\n if (typeof syncCallback === 'function')\n that.socket.send(message, syncCallback);\n else\n that.socket.send(message);\n\n if (typeof seq !== 'undefined')\n debugging('socketio') && console.debug(that.debugLabel, `sent fragment ${seq + 1}/${total} of message ${msgSequence} (${message.length} bytes)`);\n else\n debugging('socketio') && console.debug(that.debugLabel, `sent message ${msgSequence} (${message.length} bytes)`);\n }\n }\n}\n\nexports.TransportClass = SocketIOTransport;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/socketio.js?");
4674
4674
 
4675
4675
  /***/ }),
4676
4676
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcp-client",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "description": "Core libraries for accessing DCP network",
5
5
  "keywords": [
6
6
  "dcp"