diff --git a/.circleci/config.yml b/.circleci/config.yml index 9342347bdf..5ef0c6bcc0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,6 @@ -version: 2 +version: 2.1 +orbs: + node: circleci/node@5.2.0 jobs: build-macos: resource_class: macos.m1.medium.gen1 @@ -95,6 +97,44 @@ jobs: # Remove the temporary certificate file rm -f certificate.p12 + build-web: + working_directory: ~/tidepool-org/chrome-uploader + parallelism: 1 + # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did. + # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables . + environment: + HOMEBREW_NO_AUTO_UPDATE: 1 + BASH_ENV: ".circleci/bash_env.sh" + DISPLAY: ":99" + docker: + - image: cimg/node:18.17.1-browsers + steps: + - setup_remote_docker: + version: docker23 + - run: sudo apt-get update && sudo apt-get install -y build-essential git curl libusb-1.0 libavutil-dev libxss1 libsecret-1-dev libudev-dev libgtk-3-0 libcanberra-gtk3-module packagekit-gtk3-module chromium-browser fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatspi2.0-0 libcairo2 libcups2 libgbm1 libgdk-pixbuf2.0-0 libgtk-3-0 libpango-1.0-0 libpangocairo-1.0-0 libxcursor1 libxss1 xdg-utils xvfb libdbus-glib-1-2 libgtk-3-dev libxt6 + - checkout + # - run: mv .nvmrc .nvmrc.tmp + # - node/install: + # install-yarn: true + # node-version: '18.17.1' + # - run: mv .nvmrc.tmp .nvmrc + - run: git submodule sync + - run: git submodule update --init + - run: echo 'export PATH=${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin' >> $BASH_ENV + - restore_cache: + key: dependency-cache-web-{{ checksum "package.json" }} + - run: yarn config set cache-folder ~/.cache/yarn + - run: yarn --frozen-lockfile + - save_cache: + key: dependency-cache-web-{{ checksum "package.json" }} + paths: + - ~/.cache/yarn + - ./node_modules + # Test + - run: yarn lint + - run: Xvfb :99 -screen 0 1280x1024x24 & > /dev/null && yarn test + # Build docker image + - run: if [ -z "$CIRCLE_PR_NUMBER" ]; then ./artifact.sh; else echo "Forked repo; no docker image built."; fi build-windows: machine: image: windows-server-2022-gui:current @@ -138,6 +178,10 @@ workflows: filters: tags: only: /^v.*/ + - build-web: + filters: + tags: + only: /^v.*/ - build-windows: filters: tags: diff --git a/.config.js b/.config.js index e8d34164da..6c051b079c 100644 --- a/.config.js +++ b/.config.js @@ -15,6 +15,85 @@ * == BSD2 LICENSE == */ +import env from "./app/utils/env"; + +const serverEnvironments = { + local: { + hosts: ['localhost:3001'], + API_URL: 'http://localhost:8009', + UPLOAD_URL: 'http://localhost:8009', + DATA_URL: 'http://localhost:9220', + BLIP_URL: 'http://localhost:3000' + }, + development: { + hosts: ['localhost:31500'], + API_URL: 'http://localhost:31500', + UPLOAD_URL: 'http://localhost:31500', + DATA_URL: 'http://localhost:31500/dataservices', + BLIP_URL: 'http://localhost:31500' + }, + dev1: { + hosts: ['dev1.dev.tidepool.org'], + API_URL: 'https://dev1.dev.tidepool.org', + UPLOAD_URL: 'https://dev1.dev.tidepool.org', + DATA_URL: 'https://dev1.dev.tidepool.org/dataservices', + BLIP_URL: 'https://dev1.dev.tidepool.org' + }, + qa1: { + hosts: ['qa1.development.tidepool.org', 'dev-app.tidepool.org', 'dev-api.tidepool.org'], + API_URL: 'https://qa1.development.tidepool.org', + UPLOAD_URL: 'https://qa1.development.tidepool.org', + DATA_URL: 'https://qa1.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa1.development.tidepool.org' + }, + qa2: { + hosts: ['qa2.development.tidepool.org', 'stg-app.tidepool.org', 'stg-api.tidepool.org'], + API_URL: 'https://qa2.development.tidepool.org', + UPLOAD_URL: 'https://qa2.development.tidepool.org', + DATA_URL: 'https://qa2.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa2.development.tidepool.org' + }, + qa3: { + hosts: ['qa3.development.tidepool.org'], + API_URL: 'https://qa3.development.tidepool.org', + UPLOAD_URL: 'https://qa3.development.tidepool.org', + DATA_URL: 'https://qa3.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa3.development.tidepool.org' + }, + int: { + hosts: ['external.integration.tidepool.org', 'int-app.tidepool.org', 'int-api.tidepool.org'], + API_URL: 'https://external.integration.tidepool.org', + UPLOAD_URL: 'https://external.integration.tidepool.org', + DATA_URL: 'https://external.integration.tidepool.org/dataservices', + BLIP_URL: 'https://external.integration.tidepool.org' + }, + prd: { + hosts: ['app.tidepool.org', 'api.tidepool.org', 'prd-app.tidepool.org', 'prd-api.tidepool.org'], + API_URL: 'https://api.tidepool.org', + UPLOAD_URL: 'https://api.tidepool.org', + DATA_URL: 'https://api.tidepool.org/dataservices', + BLIP_URL: 'https://app.tidepool.org' + }, +}; + +function serverEnvFromLocation() { + const url = new URL(window.location.href); + let host = url.hostname; + if (host === 'localhost') { + host += ':' + url.port; + } + return serverEnvFromHost(host) +} + +function serverEnvFromHost(host) { + for (const [server, environment] of Object.entries(serverEnvironments)) { + if (_.includes(environment.hosts, host)) { + return server + } + } + return 'prd'; +} + function stringToBoolean(str, defaultValue) { if (str === 'true') { return true; @@ -32,14 +111,17 @@ function stringToArray(str, defaultValue) { return str.split(','); } +const selectedServerEnv = env.browser ? serverEnvFromLocation() : 'prd'; + module.exports = { // this is to always have the Bows logger turned on! // NB: it is distinct from our own "debug mode" DEBUG: stringToBoolean(process.env.DEBUG, true), // the defaults for these need to be pointing to prod - API_URL: process.env.API_URL || 'https://api.tidepool.org', - UPLOAD_URL: process.env.UPLOAD_URL || 'https://uploads.tidepool.org', - DATA_URL: process.env.DATA_URL || 'https://api.tidepool.org/dataservices', - BLIP_URL: process.env.BLIP_URL || 'https://app.tidepool.org', + API_URL: process.env.API_URL || serverEnvironments[selectedServerEnv].API_URL, + UPLOAD_URL: process.env.UPLOAD_URL || serverEnvironments[selectedServerEnv].UPLOAD_URL, + DATA_URL: process.env.DATA_URL || serverEnvironments[selectedServerEnv].DATA_URL, + BLIP_URL: process.env.BLIP_URL || serverEnvironments[selectedServerEnv].BLIP_URL, DEFAULT_TIMEZONE: process.env.DEFAULT_TIMEZONE || 'America/Los_Angeles', + I18N_ENABLED: stringToBoolean(process.env.I18N_ENABLED, false), }; diff --git a/.dockerignore b/.dockerignore index af27d5c322..88e1c42704 100755 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,8 @@ Dockerfile +Dockerfile.dev + docker-compose.yaml -.env \ No newline at end of file +.env diff --git a/.gitignore b/.gitignore index 4df06bcd8a..65285681a9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ lib-cov pids logs results -config/ +config/* +!config/local.sh +!config/local.example.js build build/Release .eslintcache diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 index ae60bc71ef..7f5742955f --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,91 @@ -FROM ubuntu:18.04 +FROM node:18.17.1-alpine as base +WORKDIR /app +RUN mkdir -p dist node_modules .yarn-cache && chown -R node:node . -ENV DEBIAN_FRONTEND noninteractive -ENV NODE_VERSION "v16.14.2" - -# Lots of packages. Some dependencies and stuff for GUI. -RUN apt-get -qq -y update && \ - apt-get -qq -y install build-essential git curl libusb-1.0 libavutil-dev libxss1 \ - libsecret-1-dev libudev-dev libgtk-3-0 libcanberra-gtk3-module packagekit-gtk3-module \ - chromium-browser - -RUN useradd -s /bin/bash node && mkdir -p /home/node/.config \ - && chown -R node:node /home/node - -# Yarn -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - - -RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list -RUN apt-get -qq -y update && apt-get -qq -y install yarn - -# Node -RUN curl -O https://nodejs.org/download/release/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz \ - && tar -xzf node-$NODE_VERSION-linux-x64.tar.gz -C /usr/local/bin - -ENV PATH=/usr/local/bin/node-$NODE_VERSION-linux-x64/bin:${PATH} - -RUN chown -R node:$(id -gn node) /home/node/.config - -WORKDIR /home/node - -RUN mkdir uploader - -ENV NODE_ENV "development" - -WORKDIR /home/node/uploader/ - -COPY entrypoint.sh entrypoint.sh +FROM base as build +ARG API_URL +ARG UPLOAD_URL +ARG DATA_URL +ARG BLIP_URL +ARG REALM_HOST +ARG PORT=3001 +ARG SERVICE_NAME=uploader +ARG ROLLBAR_POST_SERVER_TOKEN +ARG I18N_ENABLED=false +ARG RX_ENABLED=false +ARG PENDO_ENABLED=true +ARG TRAVIS_COMMIT +# Set ENV from ARGs +ENV \ + API_URL=$API_URL \ + UPLOAD_URL=$UPLOAD_URL \ + DATA_URL=$DATA_URL \ + BLIP_URL=$BLIP_URL \ + REALM_HOST=$REALM_HOST \ + PORT=$PORT \ + SERVICE_NAME=$SERVICE_NAME \ + ROLLBAR_POST_TOKEN=$ROLLBAR_POST_SERVER_TOKEN \ + I18N_ENABLED=$I18N_ENABLED \ + RX_ENABLED=$RX_ENABLED \ + PENDO_ENABLED=$PENDO_ENABLED \ + TRAVIS_COMMIT=$TRAVIS_COMMIT \ + NODE_ENV=development +# Install dependancies +RUN \ + echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ + && echo "http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ + && echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ + && apk --no-cache update \ + && apk --no-cache upgrade \ + && apk add --no-cache --virtual .build-deps alpine-sdk python3 linux-headers eudev-dev ffmpeg-dev \ + && rm -rf /var/cache/apk/* /tmp/* USER node +RUN mkdir -p /home/node/.yarn-cache /home/node/.cache/yarn +COPY --chown=node:node package.json yarn.lock ./ +RUN --mount=type=cache,target=/home/node/.yarn-cache,id=yarn,uid=1000,gid=1000 yarn install --ignore-scripts --cache-folder /home/node/.yarn-cache +# Copy source files, and possibily invalidate so we have to rebuild +COPY --chown=node:node . . +RUN npm run build-web +USER root +RUN apk del .build-deps -ENTRYPOINT ["/bin/bash", "entrypoint.sh"] +FROM base as production +ARG API_URL +ARG UPLOAD_URL +ARG DATA_URL +ARG BLIP_URL +ARG REALM_HOST +ARG PORT=3001 +ARG SERVICE_NAME=uploader +ARG ROLLBAR_POST_SERVER_TOKEN +ARG I18N_ENABLED=false +ARG RX_ENABLED=false +ARG PENDO_ENABLED=true +ARG TRAVIS_COMMIT +# Set ENV from ARGs +ENV \ + API_URL=$API_URL \ + UPLOAD_URL=$UPLOAD_URL \ + DATA_URL=$DATA_URL \ + BLIP_URL=$BLIP_URL \ + REALM_HOST=$REALM_HOST \ + PORT=$PORT \ + SERVICE_NAME=$SERVICE_NAME \ + ROLLBAR_POST_TOKEN=$ROLLBAR_POST_SERVER_TOKEN \ + I18N_ENABLED=$I18N_ENABLED \ + RX_ENABLED=$RX_ENABLED \ + PENDO_ENABLED=$PENDO_ENABLED \ + TRAVIS_COMMIT=$TRAVIS_COMMIT \ + NODE_ENV=production +# Only install dependancies needed for the production server +USER node +RUN yarn add express@4.16.3 helmet@7.0.0 body-parser@1.18.3 +# Copy only files needed to run the server +COPY --from=build /app/dist dist +COPY --from=build \ + /app/config.server.js \ + /app/package.json \ + /app/server.js \ + ./ +CMD ["node", "server.js"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100755 index 0000000000..283690b1a6 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,41 @@ +FROM ubuntu:18.04 + +ENV DEBIAN_FRONTEND noninteractive +ENV NODE_VERSION "v12.13.0" + +# Lots of packages. Some dependencies and stuff for GUI. +RUN apt-get -qq -y update && \ + apt-get -qq -y install build-essential git curl libusb-1.0 libavutil-dev libxss1 \ + libsecret-1-dev libudev-dev libgtk-3-0 libcanberra-gtk3-module packagekit-gtk3-module \ + chromium-browser + +RUN useradd -s /bin/bash node && mkdir -p /home/node/.config \ + && chown -R node:node /home/node + +# Yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - + +RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN apt-get -qq -y update && apt-get -qq -y install yarn + +# Node +RUN curl -O https://nodejs.org/download/release/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz \ + && tar -xzf node-$NODE_VERSION-linux-x64.tar.gz -C /usr/local/bin + +ENV PATH=/usr/local/bin/node-$NODE_VERSION-linux-x64/bin:${PATH} + +RUN chown -R node:$(id -gn node) /home/node/.config + +WORKDIR /home/node + +RUN mkdir uploader + +ENV NODE_ENV "development" + +WORKDIR /home/node/uploader/ + +COPY entrypoint.sh entrypoint.sh + +USER node + +ENTRYPOINT ["/bin/bash", "entrypoint.sh"] diff --git a/app/actions/async.js b/app/actions/async.js index 9eb7e60c45..7ec2dba91e 100644 --- a/app/actions/async.js +++ b/app/actions/async.js @@ -17,11 +17,12 @@ import async from 'async'; import { push } from 'connected-react-router'; -import { ipcRenderer } from 'electron'; import _ from 'lodash'; -import os from 'os'; -import { checkCacheValid } from 'redux-cache'; import semver from 'semver'; +import { get, set, del } from 'idb-keyval'; + +import { checkCacheValid } from 'redux-cache'; +import { ipcRenderer } from '../utils/ipc'; import * as actionSources from '../constants/actionSources'; import * as actionTypes from '../constants/actionTypes'; @@ -36,13 +37,14 @@ import personUtils from '../../lib/core/personUtils'; import { clinicUIDetails } from '../../lib/core/clinicUtils'; import * as sync from './sync'; import * as actionUtils from './utils'; +import env from '../utils/env'; let services = { api }; let versionInfo = {}; let hostMap = { - 'darwin': 'mac', - 'win32' : 'win', - 'linux': 'linux', + 'macOS': 'mac', + 'Windows' : 'win', + 'Linux': 'linux', }; const isBrowser = typeof window !== 'undefined'; @@ -95,13 +97,16 @@ export function doAppInit(opts, servicesToInit) { const { api, device, log } = services; dispatch(sync.initializeAppRequest()); - dispatch(sync.hideUnavailableDevices(opts.os || hostMap[os.platform()])); + log('Platform detected:', navigator.userAgentData.platform); + dispatch(sync.hideUnavailableDevices(opts.os || hostMap[navigator.userAgentData.platform])); log('Getting OS details.'); await actionUtils.initOSDetails(); ipcRenderer.on('bluetooth-pairing-request', async (event, details) => { - const displayBluetoothModal = actionUtils.makeDisplayBluetoothModal(dispatch); + const displayBluetoothModal = actionUtils.makeDisplayBluetoothModal( + dispatch + ); displayBluetoothModal((response) => { ipcRenderer.send('bluetooth-pairing-response', response); }, details); @@ -440,6 +445,7 @@ export function doUpload(deviceKey, opts, utc) { return async (dispatch, getState) => { const { devices, uploadTargetUser, working } = getState(); + const { log } = services; const targetDevice = _.get(devices, deviceKey); const driverId = _.get(targetDevice, 'source.driverId'); @@ -454,11 +460,27 @@ export function doUpload(deviceKey, opts, utc) { })); try { - ipcRenderer.send('setSerialPortFilter', filters); - opts.port = await navigator.serial.requestPort({ filters: filters }); + const existingPermissions = await navigator.serial.getPorts(); + + for (let i = 0; i < existingPermissions.length; i++) { + const { usbProductId, usbVendorId } = existingPermissions[i].getInfo(); + + for (let j = 0; j < driverManifest.usb.length; j++) { + if (driverManifest.usb[j].vendorId === usbVendorId + && driverManifest.usb[j].productId === usbProductId) { + log('Device has already been granted permission'); + opts.port = existingPermissions[i]; + } + } + } + + if (opts.port == null) { + ipcRenderer.send('setSerialPortFilter', filters); + opts.port = await navigator.serial.requestPort({ filters: filters }); + } } catch (err) { // not returning error, as we'll attempt user-space driver instead - console.log('Error:', err); + log('Error:', err); } } @@ -477,7 +499,7 @@ export function doUpload(deviceKey, opts, utc) { for (let j = 0; j < driverManifest.usb.length; j++) { if (driverManifest.usb[j].vendorId === existingPermissions[i].vendorId && driverManifest.usb[j].productId === existingPermissions[i].productId) { - console.log('Device has already been granted permission'); + log('Device has already been granted permission'); opts.hidDevice = existingPermissions[i]; } } @@ -498,7 +520,7 @@ export function doUpload(deviceKey, opts, utc) { const os = actionUtils.getOSDetails(); const version = versionInfo.semver; - console.log('Error:', err); + log('Error:', err); let hidErr = new Error(ErrorMessages.E_HID_CONNECTION); @@ -535,7 +557,7 @@ export function doUpload(deviceKey, opts, utc) { // we need to to scan for Bluetooth devices before the version check, // otherwise it doesn't count as a response to a user request anymore dispatch(sync.uploadRequest(uploadTargetUser, devices[deviceKey], utc)); - console.log('Scanning..'); + log('Scanning..'); try { await opts.ble.scan(); } catch (err) { @@ -545,7 +567,7 @@ export function doUpload(deviceKey, opts, utc) { const clinic = _.get(clinics, selectedClinicId, {}); const os = actionUtils.getOSDetails(); const version = versionInfo.semver; - console.log('Error:', err); + log('Error:', err); let btErr = new Error(ErrorMessages.E_BLUETOOTH_OFF); let errProps = { @@ -570,7 +592,7 @@ export function doUpload(deviceKey, opts, utc) { } return dispatch(sync.uploadFailure(btErr, errProps, devices[deviceKey])); } - console.log('Done.'); + log('Done.'); } dispatch(sync.versionCheckRequest()); @@ -615,19 +637,101 @@ export function doUpload(deviceKey, opts, utc) { } export function readFile(userId, deviceKey, file, extension) { - return (dispatch, getState) => { + const { log } = services; + + return async (dispatch, getState) => { if (!file) { - return; + const getFile = async () => { + dispatch(sync.choosingFile(userId, deviceKey)); + const regex = new RegExp('.+\.ibf', 'g'); + + for await (const entry of dirHandle.values()) { + log(entry); + // On Eros PDM there should only be one .ibf file + if (regex.test(entry.name)) { + file = { + handle: await entry.getFile(), + name: entry.name, + }; + } + } + }; + + let dirHandle = await get('directory'); + const version = versionInfo.semver; + + if (dirHandle) { + log(`Retrieved directory handle "${dirHandle.name}" from indexedDB.`); + if ((await dirHandle.queryPermission()) === 'granted') { + log('Permission already granted.'); + try { + await getFile(); + } catch (error) { + log('Device not ready yet or not plugged in.', error); + let err = new Error(ErrorMessages.E_NOT_YET_READY); + let errProps = { + code: 'E_NOT_YET_READY', + version: version, + }; + return dispatch(sync.readFileAborted(err, errProps)); + } + } else { + log('Requesting permission..'); + if ((await dirHandle.requestPermission()) === 'granted') { + try { + await getFile(); + } catch (err) { + // device mounted on a different drive number/letter, so we'll have to + // show directory picker again + log(err.name, err.message); + try { + dirHandle = await window.showDirectoryPicker(); + await set('directory', dirHandle); + await getFile(); + } catch (error) { + let err = new Error(`${ErrorMessages.E_READ_FILE}: ${error.message}`); + let errProps = { + code: 'E_READ_FILE', + version: version + }; + return dispatch(sync.readFileAborted(err, errProps)); + } + } + } else { + let err = new Error(ErrorMessages.E_READ_FILE); + let errProps = { + code: 'E_READ_FILE', + version: version + }; + return dispatch(sync.readFileAborted(err, errProps)); + } + } + } else { + try { + dirHandle = await window.showDirectoryPicker(); + await set('directory', dirHandle); + await getFile(); + } catch (error) { + let err = new Error(`${ErrorMessages.E_READ_FILE}: ${error.message}`); + let errProps = { + code: 'E_READ_FILE', + version: version + }; + return dispatch(sync.readFileAborted(err, errProps)); + } + } } - dispatch(sync.choosingFile(userId, deviceKey)); + const version = versionInfo.semver; - if (file.name.slice(-extension.length) !== extension) { + if (!file || file.name.slice(-extension.length) !== extension) { let err = new Error(ErrorMessages.E_FILE_EXT + extension); let errProps = { code: 'E_FILE_EXT', version: version }; + log('Wrong directory selected'); + del('directory'); return dispatch(sync.readFileAborted(err, errProps)); } else { @@ -636,7 +740,7 @@ export function readFile(userId, deviceKey, file, extension) { dispatch(sync.readFileRequest(userId, deviceKey, file.name)); }; - reader.onerror = () => { + const onError = () => { let err = new Error(ErrorMessages.E_READ_FILE + file.name); let errProps = { code: 'E_READ_FILE', @@ -645,14 +749,39 @@ export function readFile(userId, deviceKey, file, extension) { return dispatch(sync.readFileFailure(err, errProps)); }; - reader.onloadend = ((theFile) => { - return (e) => { - dispatch(sync.readFileSuccess(userId, deviceKey, e.srcElement.result)); - dispatch(doUpload(deviceKey)); + if (file.handle) { + // we're using File System Access API + dispatch(sync.readFileRequest(userId, deviceKey, file.name)); + try { + const filedata = await file.handle.arrayBuffer(); + + dispatch(sync.readFileSuccess(userId, deviceKey, filedata)); + const opts = { + filename : file.name, + filedata : filedata, + }; + return dispatch(doUpload(deviceKey, opts)); + } catch (err) { + log('Error', err); + return onError(); + } + } else { + let reader = new FileReader(); + reader.onloadstart = () => { + dispatch(sync.readFileRequest(userId, deviceKey, file.name)); }; - })(file); - reader.readAsArrayBuffer(file); + reader.onerror = onError; + + reader.onloadend = ((theFile) => { + return (e) => { + dispatch(sync.readFileSuccess(userId, deviceKey, e.srcElement.result)); + dispatch(doUpload(deviceKey)); + }; + })(file); + + reader.readAsArrayBuffer(file); + } } }; } @@ -662,6 +791,9 @@ export function doVersionCheck() { dispatch(sync.versionCheckRequest()); const { api } = services; const version = versionInfo.semver; + if(env.browser){ + return dispatch(sync.versionCheckSuccess()); + } api.upload.getVersions((err, versions) => { if (err) { return dispatch(sync.versionCheckFailure(err)); @@ -1128,10 +1260,18 @@ export function clickAddNewUser(){ export function setPage(page, actionSource = actionSources[actionTypes.SET_PAGE], metric) { return (dispatch, getState) => { - if(pagesMap[page]){ + if (pagesMap[page]) { + const pageProps = { pathname: pagesMap[page] }; + const meta = { source: actionSource }; _.assign(meta, metric); - dispatch(push({pathname: pagesMap[page], state: { meta }})); + pageProps.state = { meta }; + + const { hash } = window.location; + if (hash) { + pageProps.hash = hash; + } + dispatch(push(pageProps)); } }; } diff --git a/app/actions/sync.js b/app/actions/sync.js index ed3d7a67aa..e3865e96d0 100644 --- a/app/actions/sync.js +++ b/app/actions/sync.js @@ -759,56 +759,6 @@ export function quitAndInstall() { }; } -/* - * relating to driver updates - */ - -export function checkingForDriverUpdate() { - return { - type: ActionTypes.CHECKING_FOR_DRIVER_UPDATE, - meta: { source: actionSources[ActionTypes.CHECKING_FOR_DRIVER_UPDATE] } - }; -} - -export function driverUpdateAvailable(current, available) { - return { - type: ActionTypes.DRIVER_UPDATE_AVAILABLE, - payload: { current, available }, - meta: { source: actionSources[ActionTypes.DRIVER_UPDATE_AVAILABLE] } - }; -} - -export function driverUpdateNotAvailable() { - return { - type: ActionTypes.DRIVER_UPDATE_NOT_AVAILABLE, - meta: { source: actionSources[ActionTypes.DRIVER_UPDATE_NOT_AVAILABLE] } - }; -} - -export function dismissDriverUpdateAvailable() { - return { - type: ActionTypes.DISMISS_DRIVER_UPDATE_AVAILABLE, - meta: { source: actionSources[ActionTypes.DISMISS_DRIVER_UPDATE_AVAILABLE] } - }; -} - -export function driverInstall() { - return { - type: ActionTypes.DRIVER_INSTALL, - meta: { - source: actionSources[ActionTypes.DRIVER_INSTALL] - } - }; -} - -export function driverUpdateShellOpts(opts) { - return { - type: ActionTypes.DRIVER_INSTALL_SHELL_OPTS, - payload: { opts }, - meta: {source: actionSources[ActionTypes.DRIVER_INSTALL_SHELL_OPTS] } - }; -} - export function deviceTimeIncorrect(callback, cfg, times) { return { type: ActionTypes.DEVICE_TIME_INCORRECT, diff --git a/app/auth.js b/app/auth.js new file mode 100644 index 0000000000..0eaf5b25f2 --- /dev/null +++ b/app/auth.js @@ -0,0 +1,217 @@ +import { UserManager } from 'oidc-client-ts'; +import Keycloak from 'keycloak-js/dist/keycloak.mjs'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import { AuthProvider } from 'react-oidc-context'; +import { useSelector, useStore } from 'react-redux'; +import _ from 'lodash'; +import * as ActionTypes from './constants/actionTypes'; +import { sync, async } from './actions'; +import api from '../lib/core/api'; +import env from './utils/env'; +import { ipcRenderer } from './utils/ipc'; + +/** + * @type {Keycloak} + */ +export let keycloak = null; + +/** + * @type {UserManager} + */ +let userManager; + +export const oidcMiddleware = api => storeAPI => next => action => { + switch (action.type) { + case ActionTypes.KEYCLOAK_READY: { + const blipUrl = storeAPI.getState()?.blipUrls?.blipUrl; + if (blipUrl) { + const blipHref = new URL(blipUrl).href; + const registrationUrl = keycloak.createRegisterUrl({ + redirectUri: blipHref, + }); + ipcRenderer.send('keycloakRegistrationUrl', registrationUrl); + storeAPI.dispatch(sync.setKeycloakRegistrationUrl(registrationUrl)); + } + break; + } + case ActionTypes.SET_BLIP_URL: { + const blipUrl = action?.payload?.url; + const initialized = storeAPI.getState()?.keycloakConfig?.initialized; + if (blipUrl && initialized && keycloak) { + const blipHref = new URL(blipUrl).href; + const registrationUrl = keycloak.createRegisterUrl({ + redirectUri: blipHref, + }); + ipcRenderer.send('keycloakRegistrationUrl', registrationUrl); + storeAPI.dispatch(sync.setKeycloakRegistrationUrl(registrationUrl)); + } + break; + } + case ActionTypes.LOGOUT_REQUEST: { + userManager?.removeUser(); + break; + } + case ActionTypes.LOGOUT_SUCCESS: + case ActionTypes.LOGOUT_FAILURE: { + if (!env.electron) { + userManager?.signoutSilent(); + } + break; + } + default: { + if ( + action?.error?.status === 401 || + action?.error?.originalError?.status === 401 || + action?.error?.status === 403 || + action?.error?.originalError?.status === 403 || + action?.payload?.status === 401 || + action?.payload?.originalError?.status === 401 || + action?.payload?.status === 403 || + action?.payload?.originalError?.status === 403 + ) { + // on any action with a 401 or 403, we try to refresh the oidc token to verify + // if the user is still logged in + + userManager.signinSilent().then(user => { + if (!user) { + storeAPI.dispatch(sync.keycloakAuthRefreshError('onAuthRefreshError', null)); + storeAPI.dispatch(async.doLoggedOut()); + } + }).catch(err => { + // if the silent signin errors, we consider the user logged out + storeAPI.dispatch(sync.keycloakAuthRefreshError('onAuthRefreshError', err)); + storeAPI.dispatch(async.doLoggedOut()); + }); + } + break; + } + } + return next(action); +}; + +let refreshCount = 0; + +export const OidcWrapper = props => { + const [wrapperUserManager, setUserManager] = useState(null); + const blipUrl = useSelector(state => state.blipUrls.blipUrl); + const blipRedirect = useMemo(() => { + if (!blipUrl) return null; + const url = new URL(`${blipUrl}upload-redirect`); + return url.href; + }, [blipUrl]); + const keycloakConfig = useSelector(state => state.keycloakConfig); + const { url, realm } = keycloakConfig; + const authority = useMemo( + () => + keycloakConfig?.url && keycloakConfig?.realm ? `${keycloakConfig?.url}/realms/${keycloakConfig?.realm}` : null, + [keycloakConfig?.url, keycloakConfig?.realm], + ); + const [, updateState] = useState(); + const forceUpdate = useCallback(() => updateState({}), []); + const store = useStore(); + const isOauthRedirectRoute = /^(\/upload-redirect)/.test(window?.location?.pathname); + + useEffect(() => { + if (!authority || !blipRedirect) return; + + userManager = new UserManager({ + authority: authority, + client_id: 'tidepool-uploader-sso', + redirect_uri: blipRedirect, + response_mode: 'fragment', + monitorSession: !env.electron, + }); + + const loggedOut = () => { + store.dispatch(async.doLoggedOut()); + }; + + const loggedIn = (user) => { + store.dispatch(sync.keycloakAuthSuccess('onAuthSuccess', null)); + api.user.saveSession(user.profile.sub, user.access_token, { + noRefresh: true, + }); + if (!store.getState().loggedInUser) { + store.dispatch(async.doLogin()); + } + }; + + userManager.events.addUserSignedIn(() => { + userManager.getUser().then(loggedIn); + }); + + userManager.events.addUserLoaded(loggedIn); + + userManager.events.addAccessTokenExpired(() => { + store.dispatch(sync.keycloakTokenExpired('onTokenExpired', null)); + loggedOut(); + }); + + userManager.events.addSilentRenewError(() => { + store.dispatch(sync.keycloakAuthRefreshError('onAuthRefreshError', null)); + loggedOut(); + }); + + userManager.events.addUserUnloaded(() => { + store.dispatch(sync.keycloakAuthLogout('onAuthLogout', null)); + }); + + const keycloakInitOptions = { + checkLoginIframe: false, + enableLogging: process.env.NODE_ENV === 'development', + redirectUri: blipRedirect, + }; + + keycloak = new Keycloak({ + url: url, + realm: realm, + clientId: 'tidepool-uploader-sso', + }); + + keycloak.init(keycloakInitOptions).then(() => { + const logoutUrl = keycloak.createLogoutUrl({ + redirectUri: 'tidepooluploader://localhost/keycloak-redirect', + }); + store.dispatch(sync.keycloakReady('onReady', null, logoutUrl)); + }); + + setUserManager(userManager); + refreshCount++; + + }, [authority, blipRedirect, store, url, realm]); + + // watch for hash changes and re-instantiate the authClient and force a re-render of the provider + // incrementing externally defined `key` forces unmount/remount as provider doesn't expect to + // have the authClient refreshed and only sets up refresh timeout on mount + if(env.electron){ + const onHashChange = useCallback(async () => { + if(!await userManager.getUser()){ + refreshCount++; + forceUpdate(); + } + }, [forceUpdate]); + + useEffect(() => { + window.addEventListener('hashchange', onHashChange, false); + return () => { + window.removeEventListener('hashchange', onHashChange, false); + }; + }, [onHashChange]); + } + + if (authority && blipRedirect && wrapperUserManager && !isOauthRedirectRoute) { + return ( + + {props.children} + + ); + } + + return props.children; +}; + +export default { + keycloak, + userManager, + oidcMiddleware, +}; diff --git a/app/components/AdHocModal.js b/app/components/AdHocModal.js index 43166770b4..9754325bbd 100644 --- a/app/components/AdHocModal.js +++ b/app/components/AdHocModal.js @@ -25,9 +25,7 @@ import { sync as syncActions } from '../actions/'; import styles from '../../styles/components/AdHocModal.module.less'; import step1_img from '../../images/adhoc_s1.png'; import step2_img from '../../images/adhoc_s2.png'; - -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export class AdHocModal extends Component { handleContinue = () => { diff --git a/app/components/BluetoothModal.js b/app/components/BluetoothModal.js index dd85e26575..c39975b2cc 100644 --- a/app/components/BluetoothModal.js +++ b/app/components/BluetoothModal.js @@ -24,8 +24,7 @@ import { sync as syncActions } from '../actions/'; import styles from '../../styles/components/BluetoothModal.module.less'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export class BluetoothModal extends Component { handleContinue = () => { @@ -124,7 +123,7 @@ export class BluetoothModal extends Component { ); } - } + } } }; diff --git a/app/components/ClinicUploadDone.js b/app/components/ClinicUploadDone.js index 54591f4986..afa3183b2a 100644 --- a/app/components/ClinicUploadDone.js +++ b/app/components/ClinicUploadDone.js @@ -21,8 +21,7 @@ var PropTypes = require('prop-types'); var styles = require('../../styles/components/ClinicUploadDone.module.less'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; class ClinicUploadDone extends React.Component { static propTypes = { diff --git a/app/components/ClinicUserBlock.js b/app/components/ClinicUserBlock.js index f066744118..95d6b431cc 100644 --- a/app/components/ClinicUserBlock.js +++ b/app/components/ClinicUserBlock.js @@ -22,8 +22,7 @@ var sundial = require('sundial'); var personUtils = require('../../lib/core/personUtils'); var cx = require('classnames'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; var styles = require('../../styles/components/ClinicUserBlock.module.less'); diff --git a/app/components/ClinicUserEdit.js b/app/components/ClinicUserEdit.js index 276ffb3d19..479c555627 100644 --- a/app/components/ClinicUserEdit.js +++ b/app/components/ClinicUserEdit.js @@ -25,8 +25,7 @@ var sundial = require('sundial'); var personUtils = require('../../lib/core/personUtils'); var styles = require('../../styles/components/ClinicUserEdit.module.less'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; function zeroPad(value){ return _.padStart(value, 2, '0'); diff --git a/app/components/ClinicUserSelect.js b/app/components/ClinicUserSelect.js index 1ed6025fb2..47cb1ccc39 100644 --- a/app/components/ClinicUserSelect.js +++ b/app/components/ClinicUserSelect.js @@ -28,8 +28,7 @@ var styles = require('../../styles/components/ClinicUserSelect.module.less'); import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; import api from '../../lib/core/api'; diff --git a/app/components/DeviceSelection.js b/app/components/DeviceSelection.js index d539ff3eb7..2753456732 100644 --- a/app/components/DeviceSelection.js +++ b/app/components/DeviceSelection.js @@ -19,20 +19,12 @@ var _ = require('lodash'); var PropTypes = require('prop-types'); var React = require('react'); var cx = require('classnames'); -var node_os = require('os'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; import { urls } from '../constants/otherConstants'; import styles from '../../styles/components/DeviceSelection.module.less'; -var hostMap = { - 'darwin': 'mac', - 'win32' : 'win', - 'linux': 'linux', -}; - class DeviceSelection extends React.Component { static propTypes = { disabled: PropTypes.bool.isRequired, diff --git a/app/components/DeviceTimeModal.js b/app/components/DeviceTimeModal.js index 3acc296b95..6ad4c290f2 100644 --- a/app/components/DeviceTimeModal.js +++ b/app/components/DeviceTimeModal.js @@ -25,8 +25,7 @@ import { sync as syncActions } from '../actions/'; import styles from '../../styles/components/DeviceTimeModal.module.less'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export class DeviceTimeModal extends Component { determineDeviceType = () => { diff --git a/app/components/Footer.js b/app/components/Footer.js index 12fa02187b..7db1f00b5e 100644 --- a/app/components/Footer.js +++ b/app/components/Footer.js @@ -22,10 +22,10 @@ import React, { Component } from 'react'; import styles from '../../styles/components/Footer.module.less'; import logo from '../../images/JDRF_Reverse_Logo x2.png'; import debugMode from '../utils/debugMode'; +import env from '../utils/env'; import { getOSDetails } from '../actions/utils'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export default class Footer extends Component { static propTypes = { @@ -33,17 +33,25 @@ export default class Footer extends Component { }; render() { - const version = this.props.version; + const {version} = this.props; let osArch = ''; let environment = ''; + let betaWarning = ''; if (debugMode.isDebug) { osArch = ` (${getOSDetails()})`; environment = ` - ${this.props.environment}`; } + if(env.browser){ + betaWarning = (
+
Tidepool Web Uploader BETA
+
); + } + return (
+ {betaWarning}
{i18n.t('Get Support')} diff --git a/app/components/Header.js b/app/components/Header.js index 87b6cbb3e9..c4cbcfe7bf 100644 --- a/app/components/Header.js +++ b/app/components/Header.js @@ -34,8 +34,7 @@ import api from '../../lib/core/api'; import styles from '../../styles/components/Header.module.less'; import logo from '../../images/Tidepool_Logo_Light x2.png'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export class Header extends Component { static propTypes = { diff --git a/app/components/Loading.js b/app/components/Loading.js index 1ef3284c8d..3a5a3ae8c7 100644 --- a/app/components/Loading.js +++ b/app/components/Loading.js @@ -17,8 +17,7 @@ var React = require('react'); var styles = require('../../styles/components/App.module.less'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; class Loading extends React.Component { render() { diff --git a/app/components/LoggedInAs.js b/app/components/LoggedInAs.js index 25d7eb8480..c1106141ae 100644 --- a/app/components/LoggedInAs.js +++ b/app/components/LoggedInAs.js @@ -17,9 +17,9 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { ipcRenderer } from 'electron'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); + +import { ipcRenderer } from '../utils/ipc'; +import { i18n } from '../utils/config.i18next'; import personUtils from '../../lib/core/personUtils'; import api from '../../lib/core/api'; import * as metrics from '../constants/metrics'; diff --git a/app/components/LoggedOut.js b/app/components/LoggedOut.js index 5d939fff5d..0b30f4e0d7 100644 --- a/app/components/LoggedOut.js +++ b/app/components/LoggedOut.js @@ -8,8 +8,7 @@ import { pages } from '../constants/otherConstants'; import * as actionSources from '../constants/actionSources'; import logo from '../../images/Tidepool_Logo_Light x2.png'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal('i18n'); +import { i18n } from '../utils/config.i18next'; export const LoggedOut = () => { const dispatch = useDispatch(); diff --git a/app/components/Login.js b/app/components/Login.js index 78f0528e38..0e691961a4 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -18,16 +18,15 @@ import React, { useState } from 'react'; import styles from '../../styles/components/Login.module.less'; import { useDispatch, useSelector } from 'react-redux'; +import env from '../utils/env'; +import { i18n } from '../utils/config.i18next'; +import { useAuth } from 'react-oidc-context'; import actions from '../actions/'; const asyncActions = actions.async; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); - -import { keycloak } from '../keycloak'; - export const Login = () => { + const auth = useAuth(); const dispatch = useDispatch(); const disabled = useSelector((state) => Boolean(state.unsupported)); const errorMessage = useSelector((state) => state.loginErrorMessage); @@ -38,11 +37,14 @@ export const Login = () => { (state) => state.working.loggingIn.inProgress ); const keycloakConfig = useSelector((state) => state.keycloakConfig); - const keycloakInitializing = keycloakConfig.url && !keycloakConfig.initialized; const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [remember, setRemember] = useState(true); + if(auth?.user){ + auth.events._raiseUserSignedIn(auth.user); + } + const renderForgotPasswordLink = () => { return ( @@ -52,13 +54,13 @@ export const Login = () => { }; const renderButton = () => { - var text = i18n.t('Log in'); + let text = i18n.t('Log in'); if (isLoggingIn) { text = i18n.t('Logging in...'); } - if (keycloakInitializing) { + if (auth?.isLoading) { text = i18n.t('Loading...'); } @@ -67,17 +69,23 @@ export const Login = () => { type="submit" className={styles.button} onClick={handleLogin} - disabled={isLoggingIn || disabled || keycloakInitializing} + disabled={isLoggingIn || disabled || auth?.isLoading} > {text} ); }; + const redirectUri = window.location.origin + (env.electron ? '' : '/uploader'); + const handleLogin = (e) => { e.preventDefault(); if (keycloakConfig.initialized) { - window.open(keycloak.createLoginUrl(), '_blank'); + if (env.electron_renderer) { + auth.signinRedirect(); + } else { + auth.signinRedirect({redirect_uri: redirectUri}); + } } else { dispatch(asyncActions.doLogin({ username, password }, { remember })); } @@ -91,7 +99,7 @@ export const Login = () => { return {i18n.t(errorMessage)}; }; - return keycloakConfig.url ? ( + return keycloakConfig.url || auth?.isLoading ? (
{renderButton()}
) : (
diff --git a/app/components/NoUploadTargets.js b/app/components/NoUploadTargets.js index b6c57b5c82..884335452c 100644 --- a/app/components/NoUploadTargets.js +++ b/app/components/NoUploadTargets.js @@ -23,8 +23,7 @@ import personUtils from '../../lib/core/personUtils'; import styles from '../../styles/components/NoUploadTargets.module.less'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export default class NoUploadTargets extends Component { static propTypes = { diff --git a/app/components/PatientLimitModal.js b/app/components/PatientLimitModal.js index 5dc247a583..9cb90ed259 100644 --- a/app/components/PatientLimitModal.js +++ b/app/components/PatientLimitModal.js @@ -6,8 +6,7 @@ import styles from '../../styles/components/PatientLimitModal.module.less'; import { sync as syncActions } from '../actions/'; import { URL_TIDEPOOL_PLUS_PLANS } from '../constants/otherConstants'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal('i18n'); +import { i18n } from '../utils/config.i18next'; const PatientLimitModal = () => { const showingPatientLimitModal = useSelector( diff --git a/app/components/TimezoneDropdown.js b/app/components/TimezoneDropdown.js index eaa4f40ee8..b7bbb0d988 100644 --- a/app/components/TimezoneDropdown.js +++ b/app/components/TimezoneDropdown.js @@ -24,8 +24,7 @@ var cx = require('classnames'); var styles = require('../../styles/components/TimezoneDropdown.module.less'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; class TimezoneDropdown extends React.Component { constructor(props) { diff --git a/app/components/UpdateDriverModal.js b/app/components/UpdateDriverModal.js deleted file mode 100644 index c022b60d20..0000000000 --- a/app/components/UpdateDriverModal.js +++ /dev/null @@ -1,112 +0,0 @@ -/* -* == BSD2 LICENSE == -* Copyright (c) 2014-2016, Tidepool Project -* -* This program is free software; you can redistribute it and/or modify it under -* the terms of the associated License, which is identical to the BSD 2-Clause -* License as published by the Open Source Initiative at opensource.org. -* -* This program is distributed in the hope that it will be useful, but WITHOUT -* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -* FOR A PARTICULAR PURPOSE. See the License for more details. -* -* You should have received a copy of the License along with this program; if -* not, you can obtain one from Tidepool Project at tidepool.org. -* == BSD2 LICENSE == -*/ - -import _ from 'lodash'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import sudo from 'sudo-prompt'; - -import { sync as syncActions } from '../actions/'; - -import styles from '../../styles/components/UpdateDriverModal.module.less'; - -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); - -export class UpdateDriverModal extends Component { - handleInstall = () => { - const { sync, driverUpdateShellOpts } = this.props; - const { execString, options } = driverUpdateShellOpts.opts; - sudo.exec(execString, options, - (error, stdout, stderr) => { - console.log('sudo result: ' + stdout); - if (error) { - console.log(error); - } - sync.driverInstall(); - } - ); - }; - - render() { - const { - checkingDriverUpdate, - driverUpdateAvailable, - driverUpdateAvailableDismissed, - driverUpdateComplete, - sync - } = this.props; - - let title, text, actions; - - if(driverUpdateAvailableDismissed || driverUpdateComplete || !driverUpdateAvailable){ - return null; - } - - if (checkingDriverUpdate){ - title = i18n.t('Checking for driver update...'); - } else { - if (driverUpdateAvailable) { - title = i18n.t('Driver Update Available!'); - text = i18n.t('After clicking Install, the uploader will ask for your password to complete the installation. This window will close when completed.'); - actions = [ - , - - ]; - } - } - - return ( -
-
-
- {title} -
-
- {text} -
-
- {actions} -
-
-
- ); - } -}; - -export default connect( - (state, ownProps) => { - return { - // plain state - checkingDriverUpdate: state.working.checkingDriverUpdate.inProgress, - driverUpdateAvailableDismissed: state.driverUpdateAvailableDismissed, - driverUpdateAvailable: state.driverUpdateAvailable, - driverUpdateShellOpts: state.driverUpdateShellOpts, - driverUpdateComplete: state.driverUpdateComplete - }; - }, - (dispatch) => { - return { - sync: bindActionCreators(syncActions, dispatch) - }; - } -)(UpdateDriverModal); diff --git a/app/components/UpdateModal.js b/app/components/UpdateModal.js index b190bcee68..c02bd15ce0 100644 --- a/app/components/UpdateModal.js +++ b/app/components/UpdateModal.js @@ -19,15 +19,20 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { ipcRenderer } from 'electron'; import { sync as syncActions } from '../actions/'; import config from '../../lib/config.js'; import styles from '../../styles/components/UpdateModal.module.less'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { ipcRenderer } from '../utils/ipc'; +import env from '../utils/env'; +import { i18n } from '../utils/config.i18next'; + +let remote; +if (env.electron){ + remote = require('@electron/remote'); +} export class UpdateModal extends Component { handleInstall = () => { @@ -52,7 +57,7 @@ export class UpdateModal extends Component { newVersion = electronUpdateAvailable.info.version; newDate = electronUpdateAvailable.info.releaseDate.slice(0, 10); currentVersion = remote.app.getVersion(); - + if (electronUpdateAvailable.info.installDate) { currentDate = electronUpdateAvailable.info.installDate.slice(0, 10); } diff --git a/app/components/UpdatePlease.js b/app/components/UpdatePlease.js index ed18d12f23..b3e16bb334 100644 --- a/app/components/UpdatePlease.js +++ b/app/components/UpdatePlease.js @@ -20,8 +20,7 @@ import PropTypes from 'prop-types'; import styles from '../../styles/components/VersionCheck.module.less'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export default class UpdatePlease extends Component { static propTypes = { @@ -45,7 +44,7 @@ export default class UpdatePlease extends Component {

{updateText.NEEDS_UPDATED}

{updateText.IMPROVEMENTS}

- {i18n.t('Follow')} {i18n.t('these instructions')} {i18n.t('to do so.')}} + {i18n.t('Follow')} {i18n.t('these instructions')} {i18n.t('to do so.')}

diff --git a/app/components/Upload.js b/app/components/Upload.js index be72f587ea..6cc0c1c0ec 100644 --- a/app/components/Upload.js +++ b/app/components/Upload.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import Select from 'react-select'; import sundial from 'sundial'; -import keytar from 'keytar'; import BLE from 'ble-glucose'; import pako from 'pako'; @@ -34,9 +33,13 @@ import uploadDataPeriod from '../utils/uploadDataPeriod'; import { VerioBLE } from '../../lib/drivers/onetouch/oneTouchVerioBLE'; import styles from '../../styles/components/Upload.module.less'; +import env from '../utils/env'; +let keytar; +if(env.electron_renderer){ + keytar = require('keytar'); +} -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; const MEDTRONIC_KEYTAR_SERVICE = 'org.tidepool.uploader.medtronic.serialnumber'; const ble = new BLE(); @@ -104,18 +107,34 @@ export default class Upload extends Component { } populateRememberedSerialNumber() { - keytar.getPassword(MEDTRONIC_KEYTAR_SERVICE, this.props.targetId) - .then((serialNumber) => { - if(serialNumber) { - this.setState({ - medtronicSerialNumberValue: serialNumber, - medtronicSerialNumberRemember: true, - medtronicFormIncomplete: false, - }); - } - }); + if(env.electron_renderer){ + keytar.getPassword(MEDTRONIC_KEYTAR_SERVICE, this.props.targetId) + .then((serialNumber) => { + if(serialNumber) { + this.setState({ + medtronicSerialNumberValue: serialNumber, + medtronicSerialNumberRemember: true, + medtronicFormIncomplete: false, + }); + } + }); + } } + handleCareLinkUpload = () => { + /* + Once everyone has switched away from the CareLink option, this function, as + well as the props addDevice, removeDevice and onDone can be removed from + Upload, UploadList and MainPage components. See following PR for details: + https://github.com/tidepool-org/chrome-uploader/pull/602 + */ + var addDevice = this.props.addDevice.bind(null, this.props.targetId); + var removeDevice = this.props.removeDevice.bind(null, this.props.targetId); + addDevice('medtronic'); + removeDevice('carelink'); + this.props.onDone(); + }; + handleMedtronicUpload() { if (this.state.medtronicSerialNumberRemember) { // Only set the password if it is different @@ -156,6 +175,11 @@ export default class Upload extends Component { this.props.onUpload(options); } + handleOmnipodErosUpload() { + const { upload } = this.props; + this.props.readFile(null, upload.source.extension); + } + handleReset = e => { if (e) { e.preventDefault(); @@ -192,6 +216,11 @@ export default class Upload extends Component { return this.handleBluetoothUpload(_.get(upload, 'key', null)); } + if (env.browser && _.get(upload, 'key', null) === 'omnipoderos') { + // we use File System Access API only in the browser; Electron uses Node.js File API + return this.handleOmnipodErosUpload(); + } + var options = {}; this.props.onUpload(options); }; @@ -718,7 +747,8 @@ export default class Upload extends Component { isBlockModeFileChosen() { const { upload } = this.props; - if (_.get(upload, 'source.type', null) !== 'block') { + if (_.get(upload, 'source.type', null) !== 'block' && + _.get(upload, 'key', null) !== 'omnipoderos') { return false; } else { diff --git a/app/components/UploadList.js b/app/components/UploadList.js index ca523a7479..a33c0952bf 100644 --- a/app/components/UploadList.js +++ b/app/components/UploadList.js @@ -29,8 +29,7 @@ import styles from '../../styles/components/UploadList.module.less'; import Email from '@mui/icons-material/Email'; import CheckCircle from '@mui/icons-material/CheckCircle'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export default class UploadList extends Component { static propTypes = { diff --git a/app/components/UserDropdown.js b/app/components/UserDropdown.js index 8dd9f2dbcb..4beb12600a 100644 --- a/app/components/UserDropdown.js +++ b/app/components/UserDropdown.js @@ -24,8 +24,7 @@ var pagesMap = require('../constants/otherConstants').pagesMap; var styles = require('../../styles/components/UserDropdown.module.less'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; class UserDropdown extends React.Component { static propTypes = { diff --git a/app/components/VersionCheckError.js b/app/components/VersionCheckError.js index 41e8b2bc12..69949e44c6 100644 --- a/app/components/VersionCheckError.js +++ b/app/components/VersionCheckError.js @@ -24,8 +24,7 @@ import ErrorMessages from '../constants/errorMessages'; import styles from '../../styles/components/VersionCheck.module.less'; import CloudOff from '@mui/icons-material/CloudOff'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; export default class VersionCheckError extends Component { static propTypes = { diff --git a/app/components/ViewDataLink.js b/app/components/ViewDataLink.js index 4ee4c0cb87..4cc07893ca 100644 --- a/app/components/ViewDataLink.js +++ b/app/components/ViewDataLink.js @@ -21,8 +21,7 @@ var React = require('react'); var styles = require('../../styles/components/ViewDataLink.module.less'); -const remote = require('@electron/remote'); -const i18n = remote.getGlobal( 'i18n' ); +import { i18n } from '../utils/config.i18next'; class ViewDataLink extends React.Component { static propTypes = { diff --git a/app/constants/actionTypes.js b/app/constants/actionTypes.js index 93402fe0ef..327f5bf8d6 100644 --- a/app/constants/actionTypes.js +++ b/app/constants/actionTypes.js @@ -173,13 +173,6 @@ export const AUTO_UPDATE_CHECKING_FOR_UPDATES = 'AUTO_UPDATE_CHECKING_FOR_UPDATE export const MANUAL_UPDATE_CHECKING_FOR_UPDATES = 'MANUAL_UPDATE_CHECKING_FOR_UPDATES'; export const QUIT_AND_INSTALL = 'QUIT_AND_INSTALL'; -// driver update -export const CHECKING_FOR_DRIVER_UPDATE = 'CHECKING_FOR_DRIVER_UPDATE'; -export const DRIVER_UPDATE_AVAILABLE = 'DRIVER_UPDATE_AVAILABLE'; -export const DRIVER_UPDATE_NOT_AVAILABLE = 'DRIVER_UPDATE_NOT_AVAILABLE'; -export const DISMISS_DRIVER_UPDATE_AVAILABLE = 'DISMISS_DRIVER_UPDATE_AVAILABLE'; -export const DRIVER_INSTALL = 'DRIVER_INSTALL'; -export const DRIVER_INSTALL_SHELL_OPTS = 'DRIVER_INSTALL_SHELL_OPTS'; export const DEVICE_TIME_INCORRECT = 'DEVICE_TIME_INCORRECT'; export const DISMISS_DEVICE_TIME_PROMPT = 'DISMISS_DEVICE_TIME_PROMPT'; diff --git a/app/constants/actionWorkingMap.js b/app/constants/actionWorkingMap.js index cf6e857b78..83cf7e977e 100644 --- a/app/constants/actionWorkingMap.js +++ b/app/constants/actionWorkingMap.js @@ -74,11 +74,6 @@ export default (type) => { case types.FETCH_CLINIC_PATIENT_COUNT_SETTINGS_FAILURE: return 'fetchingClinicPatientCountSettings'; - case types.CHECKING_FOR_DRIVER_UPDATE: - case types.DRIVER_UPDATE_AVAILABLE: - case types.DRIVER_UPDATE_NOT_AVAILABLE: - return 'checkingDriverUpdate'; - case types.AUTO_UPDATE_CHECKING_FOR_UPDATES: case types.MANUAL_UPDATE_CHECKING_FOR_UPDATES: case types.UPDATE_AVAILABLE: diff --git a/app/constants/errorMessages.js b/app/constants/errorMessages.js index 91ab086d61..8416478eda 100644 --- a/app/constants/errorMessages.js +++ b/app/constants/errorMessages.js @@ -43,6 +43,7 @@ module.exports = { E_BLUETOOTH_OFF: 'Make sure your Bluetooth is switched on', E_DEXCOM_CONNECTION: 'Tidepool is having trouble connecting to your Dexcom receiver. Please make sure your other Dexcom uploading software programs are closed (like Dexcom Clarity or Glooko/Diasend). You can also try using another micro-USB cable. Some micro-USB cables are designed to carry a signal for power only.', E_USB_CABLE: 'Your device doesn\'t appear to be connected. You can try using another USB cable, as some USB cables are designed to carry a signal for power only.', + E_NOT_YET_READY: 'Your device is not yet ready or not plugged in', E_NO_NEW_RECORDS: 'No new records to upload', E_DATETIME_SET_BY_PUMP: 'Please correct the date/time on the linked pump.', ERR_FETCHING_CLINICS_FOR_CLINICIAN: 'Something went wrong while getting clinics for clinician.', diff --git a/app/containers/App.js b/app/containers/App.js index 3bbc30c37c..f4d5299424 100644 --- a/app/containers/App.js +++ b/app/containers/App.js @@ -20,14 +20,15 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { ipcRenderer } from 'electron'; +import * as metrics from '../constants/metrics'; import { Route, Switch } from 'react-router-dom'; -import dns from 'dns'; +import { hot } from 'react-hot-loader'; import bows from 'bows'; import config from '../../lib/config.js'; import api from '../../lib/core/api'; +import env from '../utils/env'; import device from '../../lib/core/device.js'; import localStore from '../../lib/core/localStore.js'; @@ -37,7 +38,6 @@ const asyncActions = actions.async; const syncActions = actions.sync; import { urls, pagesMap, paths } from '../constants/otherConstants'; -import { checkVersion } from '../utils/drivers'; import debugMode from '../utils/debugMode'; import MainPage from './MainPage'; @@ -53,7 +53,6 @@ import VersionCheckError from '../components/VersionCheckError'; import Footer from '../components/Footer'; import Header from '../components/Header'; import UpdateModal from '../components/UpdateModal'; -import UpdateDriverModal from '../components/UpdateDriverModal'; import DeviceTimeModal from '../components/DeviceTimeModal'; import AdHocModal from '../components/AdHocModal'; import BluetoothModal from '../components/BluetoothModal'; @@ -61,8 +60,13 @@ import PatientLimitModal from '../components/PatientLimitModal.js'; import LoggedOut from '../components/LoggedOut.js'; import styles from '../../styles/components/App.module.less'; +import { ipcRenderer } from '../utils/ipc'; -const remote = require('@electron/remote'); +let remote, dns; +if(env.electron_renderer){ + remote = require('@electron/remote'); + dns = require('dns'); +} const serverdata = { Local: { @@ -71,30 +75,30 @@ const serverdata = { DATA_URL: 'http://localhost:9220', BLIP_URL: 'http://localhost:3000' }, - Development: { - API_URL: 'https://dev-api.tidepool.org', - UPLOAD_URL: 'https://dev-uploads.tidepool.org', - DATA_URL: 'https://dev-api.tidepool.org/dataservices', - BLIP_URL: 'https://dev-app.tidepool.org' + QA1: { + API_URL: 'https://qa1.development.tidepool.org', + UPLOAD_URL: 'https://qa1.development.tidepool.org', + DATA_URL: 'https://qa1.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa1.development.tidepool.org' }, - Staging: { - API_URL: 'https://stg-api.tidepool.org', - UPLOAD_URL: 'https://stg-uploads.tidepool.org', - DATA_URL: 'https://stg-api.tidepool.org/dataservices', - BLIP_URL: 'https://stg-app.tidepool.org' + QA2: { + API_URL: 'https://qa2.development.tidepool.org', + UPLOAD_URL: 'https://qa2.development.tidepool.org', + DATA_URL: 'https://qa2.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa2.development.tidepool.org' }, Integration: { - API_URL: 'https://int-api.tidepool.org', - UPLOAD_URL: 'https://int-uploads.tidepool.org', - DATA_URL: 'https://int-api.tidepool.org/dataservices', - BLIP_URL: 'https://int-app.tidepool.org' + API_URL: 'https://external.integration.tidepool.org', + UPLOAD_URL: 'https://external.integration.tidepool.org', + DATA_URL: 'https://external.integration.tidepool.org/dataservices', + BLIP_URL: 'https://external.integration.tidepool.org' }, Production: { API_URL: 'https://api.tidepool.org', - UPLOAD_URL: 'https://uploads.tidepool.org', + UPLOAD_URL: 'https://api.tidepool.org', DATA_URL: 'https://api.tidepool.org/dataservices', BLIP_URL: 'https://app.tidepool.org' - } + }, }; export class App extends Component { @@ -109,7 +113,7 @@ export class App extends Component { this.log = bows('App'); let initial_server = _.findKey(serverdata, (key) => key.BLIP_URL === config.BLIP_URL); const selectedEnv = localStore.getItem('selectedEnv'); - if (selectedEnv) { + if (selectedEnv && env.electron) { let parsedEnv = JSON.parse(selectedEnv); console.log('setting initial server from localstore:', parsedEnv.environment); api.setHosts(parsedEnv); @@ -118,11 +122,11 @@ export class App extends Component { this.state = { server: initial_server }; + } UNSAFE_componentWillMount(){ - checkVersion(this.props.dispatch); - const selectedEnv = localStore.getItem('selectedEnv') + const selectedEnv = localStore.getItem('selectedEnv') && env.electron ? JSON.parse(localStore.getItem('selectedEnv')) : null; @@ -137,24 +141,30 @@ export class App extends Component { ); }); - dns.resolveSrv('environments-srv.tidepool.org', (err, servers) => { - if (err) { - this.log(`DNS resolver error: ${err}. Retrying...`); - dns.resolveSrv('environments-srv.tidepool.org', (err2, servers2) => { - if (!err2) { - this.addServers(servers2); - } - }); - } else { - this.addServers(servers); - } - }); + if (env.electron_renderer) { + dns.resolveSrv('environments-srv.tidepool.org', (err, servers) => { + if (err) { + this.log(`DNS resolver error: ${err}. Retrying...`); + dns.resolveSrv('environments-srv.tidepool.org', (err2, servers2) => { + if (!err2) { + this.addServers(servers2); + } + }); + } else { + this.addServers(servers); + } + }); + } - window.addEventListener('contextmenu', this.handleContextMenu, false); + if (env.electron) { + window.addEventListener('contextmenu', this.handleContextMenu, false); + } } componentWillUnmount(){ - window.removeEventListener('contextmenu', this.handleContextMenu, false); + if(env.electron){ + window.removeEventListener('contextmenu', this.handleContextMenu, false); + } } addServers = (servers) => { @@ -192,7 +202,7 @@ export class App extends Component { if (err) { this.log(`Error getting server info: ${err}`); } else { - if (_.get(configInfo, 'auth')) { + if (_.get(configInfo, 'auth') && env.electron_renderer) { ipcRenderer.send('keycloakInfo', configInfo.auth); } @@ -223,7 +233,6 @@ export class App extends Component { {/* VersionCheck as overlay */} {this.renderVersionCheck()} - @@ -302,7 +311,7 @@ export class App extends Component { App.propTypes = {}; -export default connect( +export default hot(module)(connect( (state, ownProps) => { return { // plain state @@ -321,4 +330,4 @@ export default connect( dispatch: dispatch }; } -)(App); +)(App)); diff --git a/app/containers/MainPage.js b/app/containers/MainPage.js index 6ba64ae804..e2f7eb3981 100644 --- a/app/containers/MainPage.js +++ b/app/containers/MainPage.js @@ -32,12 +32,11 @@ import UploadList from '../components/UploadList'; import ViewDataLink from '../components/ViewDataLink'; import UserDropdown from '../components/UserDropdown'; import { checkTimezoneName } from 'sundial'; -const remote = require('@electron/remote'); const asyncActions = actions.async; const syncActions = actions.sync; -const i18n = remote.getGlobal('i18n'); +import { i18n } from '../utils/config.i18next'; export class MainPage extends Component { handleClickEditUser = () => { diff --git a/app/containers/SettingsPage.js b/app/containers/SettingsPage.js index 7588a5f78e..06203dcac0 100644 --- a/app/containers/SettingsPage.js +++ b/app/containers/SettingsPage.js @@ -28,12 +28,11 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import actions from '../actions/'; import { checkTimezoneName } from 'sundial'; -const remote = require('@electron/remote'); const asyncActions = actions.async; const syncActions = actions.sync; -const i18n = remote.getGlobal('i18n'); +import { i18n } from '../utils/config.i18next'; export class SettingsPage extends Component { handleClickChangePerson = (metric = {metric: {eventName: metrics.CLINIC_SEARCH_DISPLAYED}}) => { diff --git a/app/containers/Top.js b/app/containers/Top.js new file mode 100644 index 0000000000..c1f94fa376 --- /dev/null +++ b/app/containers/Top.js @@ -0,0 +1,51 @@ +import { hot } from 'react-hot-loader'; +import rollbar from '../utils/rollbar'; +import _ from 'lodash'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Route } from 'react-router-dom'; +import { push } from 'connected-react-router'; +import { ConnectedRouter } from 'connected-react-router'; +import { createBrowserHistory, createMemoryHistory } from 'history'; + +import env from '../utils/env'; +import { ipcRenderer } from '../utils/ipc'; +import config from '../../lib/config'; +window.DEBUG = config.DEBUG; +import configureStore from '../store/configureStore'; +import App from './App'; +import '../app.global.css'; +import '../../styles/main.less'; +import { OidcWrapper } from '../auth'; + +let history; +if (env.electron) { + history = createMemoryHistory(); +} else { + history = createBrowserHistory({basename: '/uploader'}); +} + +const store = configureStore(undefined, history); +store.dispatch(push('/uploader')); + +// This is the communication mechanism for receiving actions dispatched from +// the `main` Electron process. `action` should always be the resulting object +// from an action creator. +ipcRenderer.on('action', function(event, action) { + store.dispatch(action); +}); + +ipcRenderer.on('newHash', (e, hash) => { + window.location.hash = hash; +}); + +const Top = () => ( + + + + } /> + + + +); +export default hot(module)(Top); diff --git a/app/containers/WorkspacePage.js b/app/containers/WorkspacePage.js index 1423ecbed6..6f11bd0759 100644 --- a/app/containers/WorkspacePage.js +++ b/app/containers/WorkspacePage.js @@ -8,8 +8,7 @@ import * as actionSources from '../constants/actionSources'; import actions from '../actions/'; import api from '../../lib/core/api'; -const remote = require('@electron/remote'); -const i18n = remote.getGlobal('i18n'); +import { i18n } from '../utils/config.i18next'; const { async, sync } = actions; diff --git a/app/index.js b/app/index.js index 804dda1707..31f1be57c3 100755 --- a/app/index.js +++ b/app/index.js @@ -1,55 +1,12 @@ -import rollbar from './utils/rollbar'; -import _ from 'lodash'; -import React, { Fragment } from 'react'; -import { AppContainer as ReactHotAppContainer } from 'react-hot-loader'; +import React from 'react'; import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { Route } from 'react-router-dom'; -import { push } from 'connected-react-router'; -import { ipcRenderer } from 'electron'; -import { ConnectedRouter } from 'connected-react-router'; -import { createMemoryHistory } from 'history'; -import { KeycloakWrapper } from './keycloak'; - -import config from '../lib/config'; -window.DEBUG = config.DEBUG; -import configureStore from './store/configureStore'; -import App from './containers/App'; -import './app.global.css'; -import '../styles/main.less'; import localStore from '../lib/core/localStore'; -localStore.init(localStore.getInitialState(), () => {}); - -// createHashHistory collides with the keycloak library #state -// createBrowserHistory creates a login loop -const history = createMemoryHistory(); - -const store = configureStore(undefined, history); -store.dispatch(push('/')); +import Top from './containers/Top'; -// This is the communication mechanism for receiving actions dispatched from -// the `main` Electron process. `action` should always be the resulting object -// from an action creator. -ipcRenderer.on('action', function(event, action) { - store.dispatch(action); -}); - -ipcRenderer.on('newHash', (e, hash) => { - window.location.hash = hash; -}); - -const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer; +localStore.init(localStore.getInitialState(), () => {}); render( - - - - - } /> - - - - , + , document.getElementById('app') ); diff --git a/app/keycloak.js b/app/keycloak.js deleted file mode 100644 index f4e49b3103..0000000000 --- a/app/keycloak.js +++ /dev/null @@ -1,301 +0,0 @@ -import Keycloak from 'keycloak-js/dist/keycloak.js'; -import React, { useState, useMemo, useEffect, useCallback } from 'react'; -import { ReactKeycloakProvider } from '@react-keycloak/web'; -import { useSelector, useStore } from 'react-redux'; -import _ from 'lodash'; -import * as jose from 'jose'; -import * as ActionTypes from './constants/actionTypes'; -import { sync, async } from './actions'; -import api from '../lib/core/api'; -import { ipcRenderer } from 'electron'; -import rollbar from './utils/rollbar'; - -export let keycloak = null; - -let _keycloakConfig = {}; -let refreshTimeout = null; - -export const setTokenRefresh = (tokenParsed) => { - if (refreshTimeout) { - clearTimeout(refreshTimeout); - refreshTimeout = null; - } - let timeskew = keycloak?.timeSkew ?? 0; - let expiresIn = (tokenParsed.exp - new Date().getTime() / 1000 + timeskew) * 1000; - refreshTimeout = setTimeout(() => { keycloak.updateToken(-1); }, expiresIn - 10000); -}; - -const updateKeycloakConfig = (info, store) => { - if (!_.isEqual(_keycloakConfig, info)) { - if (info?.url && info?.realm) { - keycloak = new Keycloak({ - url: info.url, - realm: info.realm, - clientId: 'tidepool-uploader-sso', - }); - store.dispatch(sync.keycloakInstantiated()); - } else { - keycloak = null; - } - - _keycloakConfig = info; - } -}; - -let latestKeycloakEvent = null; - -const onKeycloakEvent = (store) => (event, error) => { - latestKeycloakEvent = event; - switch (event) { - case 'onReady': { - let logoutUrl = keycloak.createLogoutUrl({ - redirectUri: 'tidepooluploader://localhost/keycloak-redirect' - }); - store.dispatch(sync.keycloakReady(event, error, logoutUrl)); - break; - } - case 'onInitError': { - store.dispatch(sync.keycloakInitError(event, error)); - break; - } - case 'onAuthSuccess': { - store.dispatch(sync.keycloakAuthSuccess(event, error)); - api.user.saveSession(keycloak?.tokenParsed?.sub, keycloak?.token, { - noRefresh: true, - }); - store.dispatch(async.doLogin()); - break; - } - case 'onAuthError': { - store.dispatch(sync.keycloakAuthError(event, error)); - break; - } - case 'onAuthRefreshSuccess': { - store.dispatch(sync.keycloakAuthRefreshSuccess(event, error)); - break; - } - case 'onAuthRefreshError': { - store.dispatch(sync.keycloakAuthRefreshError(event, error)); - store.dispatch(async.doLoggedOut()); - break; - } - case 'onTokenExpired': { - store.dispatch(sync.keycloakTokenExpired(event, error)); - break; - } - case 'onAuthLogout': { - store.dispatch(sync.keycloakAuthLogout(event, error)); - store.dispatch(async.doLoggedOut()); - break; - } - default: - break; - } -}; - -const onKeycloakTokens = (store) => (tokens) => { - if (tokens?.token) { - store.dispatch(sync.keycloakTokensReceived(tokens)); - let tokenParsed; - try { - tokenParsed = jose.decodeJwt(tokens?.token); - } catch (e) { - if (rollbar) { - rollbar.error('keycloak token decode error', { - error: e, - tokens, - }); - } - store.dispatch(async.doLoggedOut()); - return; - } - - if(tokenParsed?.sub && tokenParsed?.exp) { - api.user.saveSession(tokenParsed.sub, tokens.token, { - noRefresh: true, - }); - - // this should be a reference to the same object property - if (tokens.token !== keycloak?.token) { - if (rollbar) { - rollbar.info('keycloak token mismatch', { - keycloakToken: keycloak?.token ? jose.decodeJwt(keycloak?.token) : {}, - tokensToken: tokenParsed, - }); - } - } - - if (!store.getState().loggedInUser) { - store.dispatch(async.doLogin()); - } - setTokenRefresh(tokenParsed); - } else { - // if we don't have a sub and exp, we can't save the session - if (rollbar) { - rollbar.error('keycloak token missing sub or exp', { - tokenParsed, - }); - } - store.dispatch(async.doLoggedOut()); - return; - } - } else { - const expectedEvents = [ - 'onReady', - 'onAuthLogout', - 'onAuthRefreshError', - 'onAuthError', - 'onInitError' - ]; - if (expectedEvents.includes(latestKeycloakEvent)) { - return; - } - - // if we don't have a token, we can't save the session - if(rollbar) { - rollbar.error('keycloak token missing', { - tokens, - }); - } - store.dispatch(async.doLoggedOut()); - } -}; - -export const keycloakMiddleware = (api) => (storeAPI) => (next) => (action) => { - switch (action.type) { - case ActionTypes.FETCH_INFO_SUCCESS: { - if (!_.isEqual(_keycloakConfig, action.payload?.info?.auth)) { - updateKeycloakConfig(action.payload?.info?.auth, storeAPI); - } - break; - } - case ActionTypes.KEYCLOAK_READY: { - let blipUrl = storeAPI.getState()?.blipUrls?.blipUrl; - if (blipUrl) { - let blipHref = new URL(blipUrl).href; - let registrationUrl = keycloak.createRegisterUrl({ - redirectUri: blipHref, - }); - ipcRenderer.send('keycloakRegistrationUrl', registrationUrl); - storeAPI.dispatch(sync.setKeycloakRegistrationUrl(registrationUrl)); - } - break; - } - case ActionTypes.SET_BLIP_URL: { - let blipUrl = action?.payload?.url; - let initialized = storeAPI.getState()?.keycloakConfig?.initialized; - if (blipUrl && initialized && keycloak) { - let blipHref = new URL(blipUrl).href; - let registrationUrl = keycloak.createRegisterUrl({ - redirectUri: blipHref, - }); - ipcRenderer.send('keycloakRegistrationUrl', registrationUrl); - storeAPI.dispatch(sync.setKeycloakRegistrationUrl(registrationUrl)); - } - break; - } - case ActionTypes.LOGOUT_SUCCESS: - case ActionTypes.LOGOUT_FAILURE: { - keycloak.clearToken(); - } - default:{ - if ( - action?.error?.status === 401 || - action?.error?.originalError?.status === 401 || - action?.error?.status === 403 || - action?.error?.originalError?.status === 403 || - action?.payload?.status === 401 || - action?.payload?.originalError?.status === 401 || - action?.payload?.status === 403 || - action?.payload?.originalError?.status === 403 - ) { - // on any action with a 401 or 403, we try to refresh to keycloak token to verify - // if the user is still logged in - keycloak.updateToken(-1); - } - break; - } - } - return next(action); -}; - -let keyCount = 0; - -export const KeycloakWrapper = (props) => { - const keycloakConfig = useSelector((state) => state.keycloakConfig); - const blipUrl = useSelector((state) => state.blipUrls.blipUrl); - const blipRedirect = useMemo(() => { - if (!blipUrl) return null; - let url = new URL(`${blipUrl}upload-redirect`); - return url.href; - }, [blipUrl]); - const [, updateState] = useState(); - const forceUpdate = useCallback(() => updateState({}), []); - const store = useStore(); - const initOptions = useMemo( - () => ({ - checkLoginIframe: false, - enableLogging: process.env.NODE_ENV === 'development', - redirectUri: blipRedirect, - }), - [blipRedirect] - ); - - const onEvent = useCallback(onKeycloakEvent(store), [store]); - const onTokens = useCallback(onKeycloakTokens(store), [store]); - - // watch for hash changes and re-instantiate the authClient and force a re-render of the provider - // incrementing externally defined `key` forces unmount/remount as provider doesn't expect to - // have the authClient refreshed and only sets up refresh timeout on mount - const onHashChange = useCallback(() => { - // we only want to do this when unauthenticated since people can hit the - // launch button multiple times - if (!keycloak?.authenticated) { - keycloak = new Keycloak({ - url: keycloakConfig.url, - realm: keycloakConfig.realm, - clientId: 'tidepool-uploader-sso', - }); - keyCount++; - forceUpdate(); - } - }, [keycloakConfig.realm, keycloakConfig.url, blipRedirect]); - - useEffect(() => { - window.addEventListener('hashchange', onHashChange, false); - return () => { - window.removeEventListener('hashchange', onHashChange, false); - }; - }, [onHashChange]); - - // clear the refresh timeout on unmount - useEffect(() => { - return () => { - if (refreshTimeout) { - clearTimeout(refreshTimeout); - refreshTimeout = null; - } - }; - }, []); - - if (keycloakConfig.url && keycloakConfig.instantiated && blipRedirect) { - return ( - - {props.children} - - ); - } else { - return {props.children}; - } -}; - -export default { - keycloak, - keycloakMiddleware, -}; diff --git a/app/main.dev.js b/app/main.dev.js index ae1fd4380d..b3efbef5d0 100755 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -9,15 +9,11 @@ import { sync as syncActions } from './actions'; import debugMode from '../app/utils/debugMode'; import Rollbar from 'rollbar/src/server/rollbar'; import uploadDataPeriod from './utils/uploadDataPeriod'; -import i18n from 'i18next'; -import i18nextBackend from 'i18next-fs-backend'; -import i18nextOptions from './utils/config.i18next'; +import { setLanguage, i18n } from './utils/config.i18next'; import path from 'path'; import fs from 'fs'; import child_process from 'child_process'; -global.i18n = i18n; - autoUpdater.logger = require('electron-log'); autoUpdater.logger.transports.file.level = 'info'; const remoteMain = require('@electron/remote/main'); @@ -51,8 +47,13 @@ const baseURL = fileURL.href; let menu; let template; + +/** + * @type {BrowserWindow} + */ let mainWindow = null; let serialPortFilter = null; +let usbFilter = null; let bluetoothPinCallback = null; // Web Bluetooth should only be an experimental feature on Linux @@ -138,7 +139,9 @@ const openExternalUrl = (url) => { app.on('ready', async () => { await installExtensions(); - setLanguage(); + setLanguage(() => { + createWindow(); + }); }); function createWindow() { @@ -182,8 +185,12 @@ function createWindow() { setRequestFilter(); }); - let setRequestFilter = () => { - let urls = ['http://localhost/keycloak-redirect*', '*://*/upload-redirect*']; + const setRequestFilter = () => { + const urls = [ + 'http://localhost/keycloak-redirect*', + '*://*/*upload-redirect*', + '*://*/*protocol/openid-connect/auth*', + ]; if (keycloakUrl && keycloakRealm) { urls.push( `${keycloakUrl}/realms/${keycloakRealm}/login-actions/registration*` @@ -192,10 +199,19 @@ function createWindow() { webRequest.onBeforeRequest({ urls }, async (request, cb) => { const requestURL = new URL(request.url); + // catch attempts to load the auth page in the app and launch externally + if (requestURL.pathname.includes('/protocol/openid-connect/auth')) { + return openExternalUrl(request.url); + } + // capture keycloak sign-in redirect - if (requestURL.pathname.includes('keycloak-redirect') || requestURL.pathname.includes('upload-redirect')) { + if ( + requestURL.pathname.includes('keycloak-redirect') || + requestURL.pathname.includes('upload-redirect') + ) { return handleIncomingUrl(request.url); } + // capture keycloak registration navigation if ( requestURL.href.includes( @@ -287,6 +303,28 @@ operating system, as soon as possible.`, } }); + mainWindow.webContents.session.on('select-usb-device', (event, details, callback) => { + event.preventDefault(); + console.log('Device list:', details.deviceList); + + let selectedDevice; + for (let i = 0; i < usbFilter.length; i++) { + selectedDevice = details.deviceList.find((element) => + usbFilter[i].vendorId === element.vendorId && + usbFilter[i].productId === element.productId + ); + if (selectedDevice) { + break; + } + } + + if (!selectedDevice) { + callback(''); + } else { + callback(selectedDevice.deviceId); + } + }); + mainWindow.webContents.session.setBluetoothPairingHandler((details, callback) => { bluetoothPinCallback = callback; console.log('Sending bluetooth pairing request to renderer'); @@ -620,6 +658,10 @@ ipcMain.on('setSerialPortFilter', (event, arg) => { serialPortFilter = arg; }); +ipcMain.on('setUSBFilter', (event, arg) => { + usbFilter = arg; +}); + ipcMain.on('bluetooth-pairing-response', (event, response) => { bluetoothPinCallback(response); }); @@ -647,19 +689,16 @@ const handleIncomingUrl = (url) => { const requestURL = new URL(url); // capture keycloak sign-in redirect if (requestURL.pathname.includes('keycloak-redirect') || requestURL.pathname.includes('upload-redirect')) { - const requestHash = requestURL.hash; if(mainWindow){ const { webContents } = mainWindow; - // redirecting from the app html to app html with hash breaks devtools - // just send and append the hash if we're already in the app html - if (webContents.getURL().includes(baseURL)) { - webContents.send('newHash', requestHash); - } else { - webContents.loadURL(`${baseURL}${requestHash}`); + const requestHash = requestURL.hash; + const newUrl = `${baseURL}${requestHash}`; + if(webContents.getURL() !== newUrl){ + webContents.loadURL(newUrl); } - return; + return; } - + } }; @@ -686,28 +725,6 @@ if (!gotTheLock) { }); } -function setLanguage() { - if (process.env.I18N_ENABLED === 'true') { - let lng = app.getLocale(); - // remove country in language locale - if (_.includes(lng,'-')) - lng = (_.split(lng,'-').length > 0) ? _.split(lng,'-')[0] : lng; - - i18nextOptions['lng'] = lng; - } - - if (!i18n.Initialize) { - i18n.use(i18nextBackend).init(i18nextOptions, function(err, t) { - if (err) { - console.log('An error occurred in i18next:', err); - } - - global.i18n = i18n; - createWindow(); - }); - } -} - function getURLFromArgs(args) { if (Array.isArray(args) && args.length) { const url = args[args.length - 1]; diff --git a/app/package.json b/app/package.json index bdf23d11b6..00424f4fb1 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "tidepool-uploader", "productName": "tidepool-uploader", - "version": "2.58.0-remove-lzo-decompress.1", + "version": "2.58.0-react-oidc-context.4", "description": "Tidepool Project Universal Uploader", "main": "./main.prod.js", "author": { diff --git a/app/reducers/devices.js b/app/reducers/devices.js index 7294d1e23e..1ed20d997a 100644 --- a/app/reducers/devices.js +++ b/app/reducers/devices.js @@ -1,10 +1,8 @@ -import os from 'os'; import mm723Image from '../../images/MM723_CNL_combo@2x.jpg'; import mm600Image from '../../images/MM600_CNL_combo@2x.jpg'; -const remote = require('@electron/remote'); - -const i18n = remote.getGlobal( 'i18n' ); +import env from '../utils/env'; +import { i18n } from '../utils/config.i18next'; const devices = { abbottfreestylelibre: { instructions: i18n.t('Plug in meter with micro-USB cable'), @@ -148,9 +146,17 @@ const devices = { enabled: {mac: true, win: true, linux: true} }, omnipod: { - instructions: [i18n.t('Classic PDM: Plug into USB. Wait for Export to complete. Click Upload.'), i18n.t('DASH PDM: Unlock. Plug into USB. Tap Export on PDM. Click Upload.')], + instructions: i18n.t('DASH PDM: Unlock. Plug into USB. Tap Export on PDM. Click Upload.'), key: 'omnipod', - name: 'Insulet Omnipod', + name: 'Insulet Omnipod DASH', + source: {type: 'device', driverId: 'InsuletOmniPod', extension: '.ibf'}, + enabled: {mac: true, win: true, linux: true}, + powerOnlyWarning: true, + }, + omnipoderos: { + instructions: i18n.t('Plug into USB. Wait for Export to complete. Click Upload.'), + key: 'omnipoderos', + name: 'Insulet Omnipod Classic', source: {type: 'device', driverId: 'InsuletOmniPod', extension: '.ibf'}, enabled: {mac: true, win: true, linux: true}, powerOnlyWarning: true, @@ -185,19 +191,19 @@ const devices = { enabled: {mac: true, win: true, linux: true} }, onetouchselect: { - instructions: i18n.t('Plug in meter with micro-USB'), + instructions: i18n.t('Plug in meter with micro-USB cable and make sure Uploader Helper extension is installed'), name: 'OneTouch Select Plus Flex', key: 'onetouchselect', source: {type: 'device', driverId: 'OneTouchSelect'}, - enabled: {linux: true, mac: true, win: true}, + enabled: {linux: false, mac: false, win: true}, powerOnlyWarning: true, }, onetouchverio: { - instructions: i18n.t('Plug in meter with micro-USB'), + instructions: i18n.t('Plug in meter with micro-USB cable and make sure Uploader Helper extension is installed'), name: 'OneTouch Verio, Verio Flex and Verio Reflect', key: 'onetouchverio', source: {type: 'device', driverId: 'OneTouchVerio'}, - enabled: {linux: true, mac: true, win: true}, + enabled: {linux: false, mac: false, win: true}, powerOnlyWarning: true, }, onetouchverioble: { @@ -284,4 +290,12 @@ if (navigator.userAgentData.platform === 'macOS') { }; } +if (env.electron) { + devices.onetouchverio.enabled = {mac: true, win: true, linux:true}; + devices.onetouchverio.instructions = i18n.t('Plug in meter with micro-USB cable'); + devices.onetouchselect.enabled = {mac: true, win: true, linux:true}; + devices.onetouchselect.instructions = i18n.t('Plug in meter with micro-USB cable'); +} + export default devices; + diff --git a/app/reducers/initialState.js b/app/reducers/initialState.js index 23617acd28..c9c9a322dd 100644 --- a/app/reducers/initialState.js +++ b/app/reducers/initialState.js @@ -39,7 +39,6 @@ const initialState = { checkingVersion: Object.assign({}, working), uploading: Object.assign({}, working), checkingElectronUpdate: Object.assign({}, working), - checkingDriverUpdate: Object.assign({}, working), fetchingClinicsForClinician: Object.assign({}, working), updatingClinicPatient: Object.assign({}, working), fetchingPatientsForClinic: Object.assign({}, working), @@ -62,10 +61,6 @@ const initialState = { electronUpdateAvailableDismissed: null, electronUpdateAvailable: null, electronUpdateDownloaded: null, - driverUpdateAvailable: null, - driverUpdateAvailableDismissed: null, - driverUpdateShellOpts: null, - driverUpdateComplete: null, showingDeviceTimePrompt: null, isTimezoneFocused: false, showingAdHocPairingDialog: false, diff --git a/app/reducers/misc.js b/app/reducers/misc.js index 201ade65c1..5b3825406b 100644 --- a/app/reducers/misc.js +++ b/app/reducers/misc.js @@ -183,47 +183,6 @@ export function electronUpdateDownloaded(state = initialState.electronUpdateDown } } -export function driverUpdateAvailable(state = initialState.driverUpdateAvailable, action) { - switch (action.type) { - case types.DRIVER_UPDATE_AVAILABLE: - return action.payload; - case types.DRIVER_UPDATE_NOT_AVAILABLE: - case types.DRIVER_INSTALL: - return false; - default: - return state; - } -} - -export function driverUpdateAvailableDismissed(state = initialState.driverUpdateAvailableDismissed, action) { - switch (action.type) { - case types.CHECKING_FOR_DRIVER_UPDATE: - return false; - case types.DISMISS_DRIVER_UPDATE_AVAILABLE: - return true; - default: - return state; - } -} - -export function driverUpdateShellOpts(state = initialState.driverUpdateShellOpts, action) { - switch (action.type) { - case types.DRIVER_INSTALL_SHELL_OPTS: - return action.payload; - default: - return state; - } -} - -export function driverUpdateComplete(state = initialState.driverUpdateComplete, action) { - switch (action.type) { - case types.DRIVER_INSTALL: - return true; - default: - return state; - } -} - export function showingDeviceTimePrompt(state = initialState.showingDeviceTimePrompt, action) { switch (action.type) { case types.DEVICE_TIME_INCORRECT: diff --git a/app/reducers/uploads.js b/app/reducers/uploads.js index f44db60936..d8061d7b9a 100644 --- a/app/reducers/uploads.js +++ b/app/reducers/uploads.js @@ -139,6 +139,8 @@ export function uploadsByUser(state = {}, action) { readingFile: {$set: false} }}} ); + } else { + return state; } } case types.READ_FILE_REQUEST: { diff --git a/app/reducers/working.js b/app/reducers/working.js index 1b4ef8f3e0..1eb0109f30 100644 --- a/app/reducers/working.js +++ b/app/reducers/working.js @@ -34,7 +34,6 @@ export default (state = initialWorkingState, action) => { case types.CREATE_CUSTODIAL_ACCOUNT_REQUEST: case types.CREATE_CLINIC_CUSTODIAL_ACCOUNT_REQUEST: case types.UPDATE_CLINIC_PATIENT_REQUEST: - case types.CHECKING_FOR_DRIVER_UPDATE: case types.VERSION_CHECK_REQUEST: case types.UPLOAD_REQUEST: case types.AUTO_UPDATE_CHECKING_FOR_UPDATES: @@ -98,8 +97,6 @@ export default (state = initialWorkingState, action) => { case types.UPLOAD_SUCCESS: case types.UPDATE_AVAILABLE: case types.UPDATE_NOT_AVAILABLE: - case types.DRIVER_UPDATE_AVAILABLE: - case types.DRIVER_UPDATE_NOT_AVAILABLE: case types.INIT_APP_SUCCESS: case types.UPDATE_PROFILE_SUCCESS: case types.FETCH_INFO_SUCCESS: diff --git a/app/static/silent-check-sso.html b/app/static/silent-check-sso.html new file mode 100644 index 0000000000..fe34a8f749 --- /dev/null +++ b/app/static/silent-check-sso.html @@ -0,0 +1,5 @@ + + + + + diff --git a/app/store/configureStore.development.js b/app/store/configureStore.development.js index e9b232bc2a..09d8d721ed 100644 --- a/app/store/configureStore.development.js +++ b/app/store/configureStore.development.js @@ -7,7 +7,7 @@ import api from '../../lib/core/api'; import config from '../../lib/config'; import { createErrorLogger } from '../utils/errors'; import { createMetricsTracker } from '../utils/metrics'; -import { keycloakMiddleware } from '../keycloak'; +import { oidcMiddleware } from '../auth'; api.create({ apiUrl: config.API_URL, @@ -31,6 +31,8 @@ export default function configureStore(initialState, history) { window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ // Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html actionCreators, + trace: true, + traceLimit: 250, }) : compose; /* eslint-enable no-underscore-dangle */ @@ -41,17 +43,17 @@ export default function configureStore(initialState, history) { router, createErrorLogger(api), createMetricsTracker(api), - keycloakMiddleware(api), + oidcMiddleware(api), ) ); const store = createStore(rootReducer(history), initialState, enhancer); - if (module.hot) { - module.hot.accept('../reducers', () => - store.replaceReducer(require('../reducers')(history)).default // eslint-disable-line global-require - ); - } + // if (module.hot) { + // module.hot.accept('../reducers', () => + // store.replaceReducer(require('../reducers')(history)).default // eslint-disable-line global-require + // ); + // } return store; } diff --git a/app/store/configureStore.production.js b/app/store/configureStore.production.js index cc632e15b7..c96dbc5662 100644 --- a/app/store/configureStore.production.js +++ b/app/store/configureStore.production.js @@ -6,7 +6,7 @@ import api from '../../lib/core/api'; import config from '../../lib/config'; import { createErrorLogger } from '../utils/errors'; import { createMetricsTracker } from '../utils/metrics'; -import { keycloakMiddleware } from '../keycloak'; +import { oidcMiddleware } from '../auth'; api.create({ apiUrl: config.API_URL, @@ -22,7 +22,7 @@ export default function configureStore(initialState, history) { router, createErrorLogger(api), createMetricsTracker(api), - keycloakMiddleware(api), + oidcMiddleware(api), ); return createStore(rootReducer(history), initialState, enhancer); // eslint-disable-line diff --git a/app/utils/config.i18next.js b/app/utils/config.i18next.js index eab6debeb8..3a94956a6d 100644 --- a/app/utils/config.i18next.js +++ b/app/utils/config.i18next.js @@ -1,21 +1,162 @@ -var path = require('path'); - -let dirPath = (process.env.NODE_ENV === 'production') ? path.join(__dirname, '../') : '.'; -let i18nextOptions = module.exports = { - backend: { - loadPath: dirPath + '/locales/{{lng}}/{{ns}}.json', - addPath: dirPath + '/locales/{{lng}}/{{ns}}.missing.json' - }, - interpolation: { - escapeValue: false - }, - lng: 'en', - saveMissing: true, - fallbackLng: 'en', - returnEmptyString: false, - supportedLngs: ['en', 'es'], - keySeparator: false, - nsSeparator: '|', - debug: false, - wait: true +import env from './env'; +import { reactI18nextModule } from 'react-i18next'; +import _ from 'lodash'; + +let i18n; +let i18nextOptions = {}; +let setLanguage; + +if (env.electron_main) { + i18n = require('i18next'); + const { app } = require('electron'); + const i18nextBackend = require('i18next-fs-backend'); + + var path = require('path'); + + let dirPath = + process.env.NODE_ENV === 'production' ? path.join(__dirname, '../') : '.'; + i18nextOptions = { + backend: { + loadPath: dirPath + '/locales/{{lng}}/{{ns}}.json', + addPath: dirPath + '/locales/{{lng}}/{{ns}}.missing.json', + }, + interpolation: { + escapeValue: false, + }, + lng: 'en', + saveMissing: true, + fallbackLng: 'en', + returnEmptyString: false, + supportedLngs: ['en', 'es'], + keySeparator: false, + nsSeparator: '|', + debug: false, + wait: true, + }; + + setLanguage = (cb) => { + cb ??= _.noop; + if (process.env.I18N_ENABLED === 'true') { + let lng = app.getLocale(); + // remove country in language locale + if (_.includes(lng, '-')) + lng = _.split(lng, '-').length > 0 ? _.split(lng, '-')[0] : lng; + + i18nextOptions['lng'] = lng; + } + + if (!i18n.isInitialized) { + i18n + .use(reactI18nextModule) + .use(i18nextBackend) + .init(i18nextOptions, function(err, t) { + if (err) { + console.log('An error occurred in i18next:', err); + } + + global.i18n = i18n; + cb(); + }); + } else { + i18n.changeLanguage(i18nextOptions.lng).then(() => { + cb(); + }); + } + }; + + setLanguage(); +} else { + if (env.electron_renderer) { + const remote = require('@electron/remote'); + i18n = remote.getGlobal('i18n'); + } +} + +if (env.browser && !env.electron_renderer) { + i18n = require('i18next'); + i18nextOptions = { + // backend: { + // loadPath: './locales/{{lng}}/{{ns}}.json', + // addPath: './locales/{{lng}}/{{ns}}.missing.json', + // }, + interpolation: { + escapeValue: false, + }, + lng: 'en', + saveMissing: true, + fallbackLng: 'en', + returnEmptyString: false, + supportedLngs: ['en', 'es'], + keySeparator: false, + nsSeparator: '|', + debug: false, + wait: true, + fallbackLng: 'en', + + // To allow . in keys + keySeparator: false, + // To allow : in keys + nsSeparator: '|', + + debug: false, + + interpolation: { + escapeValue: false, // not needed for react!! + }, + + // If the translation is empty, return the key instead + returnEmptyString: false, + + react: { + wait: true, + withRef: true, + // Needed for react < 16 + defaultTransParent: 'div', + }, + + resources: { + en: { + // Default namespace + translation: require('../../locales/en/translation.json'), + }, + es: { + // Default namespace + translation: require('../../locales/es/translation.json'), + }, + }, + }; + + let setLanguage = (cb) => { + cb ??= _.noop; + if (process.env.I18N_ENABLED === 'true') { + let lng = navigator.language; + // remove country in language locale + if (_.includes(lng, '-')) + lng = _.split(lng, '-').length > 0 ? _.split(lng, '-')[0] : lng; + + i18nextOptions['lng'] = lng; + } + + if (!i18n.isInitialized) { + i18n.use(reactI18nextModule).init(i18nextOptions, function(err, t) { + if (err) { + console.log('An error occurred in i18next:', err); + } + global.i18n = i18n; + cb(); + }); + } else { + i18n.changeLanguage(i18nextOptions.lng).then(() => { + cb(); + }); + } + }; + + setLanguage(); +} + +module.exports = { + i18nextOptions, + setLanguage, + i18n, }; diff --git a/app/utils/debugMode.js b/app/utils/debugMode.js index 0745604734..0ae14d3ccd 100644 --- a/app/utils/debugMode.js +++ b/app/utils/debugMode.js @@ -14,38 +14,39 @@ * not, you can obtain one from Tidepool Project at tidepool.org. * == BSD2 LICENSE == */ -import isElectron from 'is-electron'; -import { ipcRenderer, ipcMain } from 'electron'; -let isRenderer = (process && process.type === 'renderer'); +import env from './env'; +import _ from 'lodash'; +import { ipcRenderer, ipcMain } from './ipc'; -if (isRenderer) { - let isDebug = process.env.DEBUG_ERROR || +if (env.electron_renderer) { + let isDebug = + process.env.DEBUG_ERROR || JSON.parse(localStorage.getItem('isDebug')) || false; - const debugMode = module.exports = { + const debugMode = (module.exports = { isDebug, setDebug: function(isDebug) { ipcRenderer.send('setDebug', isDebug); localStorage.setItem('isDebug', JSON.stringify(isDebug)); debugMode.isDebug = isDebug; return debugMode.isDebug; - } - }; + }, + }); } else { - let isDebug = process.env.DEBUG_ERROR || false; + let isDebug = _.get(process, 'env.DEBUG_ERROR', false); - if (isElectron()) { + if (env.electron_main) { ipcMain.on('setDebug', (event, arg) => { debugMode.isDebug = arg; }); } - const debugMode = module.exports = { + const debugMode = (module.exports = { isDebug, setDebug: function(isDebug) { debugMode.isDebug = isDebug; return debugMode.isDebug; - } - }; + }, + }); } diff --git a/app/utils/drivers.darwin.js b/app/utils/drivers.darwin.js deleted file mode 100644 index 1588729b8c..0000000000 --- a/app/utils/drivers.darwin.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * == BSD2 LICENSE == - * Copyright (c) 2017, Tidepool Project - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the associated License, which is identical to the BSD 2-Clause - * License as published by the Open Source Initiative at opensource.org. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the License for more details. - * - * You should have received a copy of the License along with this program; if - * not, you can obtain one from Tidepool Project at tidepool.org. - * == BSD2 LICENSE == - */ - -import plist from 'plist'; -import fs from 'fs'; -import path from 'path'; -import * as sync from '../actions/sync'; - -const remote = require('@electron/remote'); - -export function checkVersion(dispatch) { - - dispatch(sync.checkingForDriverUpdate()); - - function setInstallOpts(iconsPath, scriptPath, driverPath) { - const options = { - name: 'Tidepool Driver Installer', - icns: iconsPath - }; - const execString = scriptPath.replace(/ /g, '\\ ') + ' ' + driverPath.replace(/ /g, '\\ '); - dispatch(sync.driverUpdateShellOpts({options,execString})); - } - - function readVersion(pListFile) { - try { - const list = plist.parse(fs.readFileSync(pListFile, 'utf8')); - return list.CFBundleVersion; - } catch (error) { - if (error.code === 'ENOENT') { - return 'Not found'; - } else { - console.log(error); - } - return null; - } - } - - function hasOldDriver(dPath, driverList, installPath, pListFile) { - let installedVersion, currentVersion; - for (const driver of driverList) { - if (pListFile === null) { - currentVersion = readVersion(path.join(dPath, driver, driver + '.plist')); - installedVersion = readVersion(path.join(installPath, driver + '.plist')); - } else { - currentVersion = readVersion(path.join(dPath, driver, pListFile)); - installedVersion = readVersion(path.join(installPath, driver, pListFile)); - } - console.log(driver,'version: Installed =', installedVersion, ', Current =', currentVersion); - - if(currentVersion !== installedVersion) { - dispatch(sync.driverUpdateAvailable(installedVersion, currentVersion)); - return true; - } - } - dispatch(sync.driverUpdateNotAvailable(installedVersion)); - return false; - } - - const appFolder = path.dirname(remote.app.getAppPath()); - let helperPath = path.join(appFolder, 'driver/helpers/'); - let driverPath = path.join(appFolder, 'driver/'); - let iconsPath = path.join(appFolder, '/icon.icns'); - let scriptPath = path.join(appFolder, 'driver/updateDrivers.sh'); - - if (!remote.app.isPackaged) { - driverPath = path.resolve(appFolder, 'build/driver/'); - helperPath = path.join(appFolder, 'resources/mac/helpers/'); - scriptPath = path.resolve(appFolder, 'resources/mac/updateDrivers.sh'); - } - - const helperList = fs.readdirSync(helperPath).filter(e => e[0] !== '.'); - if (hasOldDriver(helperPath, helperList, '/Library/LaunchDaemons/', null)) { - setInstallOpts(iconsPath, scriptPath, driverPath); - } -} diff --git a/app/utils/drivers.js b/app/utils/drivers.js deleted file mode 100644 index 05135dcd72..0000000000 --- a/app/utils/drivers.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * == BSD2 LICENSE == - * Copyright (c) 2015-2016, Tidepool Project - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the associated License, which is identical to the BSD 2-Clause - * License as published by the Open Source Initiative at opensource.org. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the License for more details. - * - * You should have received a copy of the License along with this program; if - * not, you can obtain one from Tidepool Project at tidepool.org. - * == BSD2 LICENSE == - */ - -import os from 'os'; - -const platform = os.platform(); -if (platform === 'darwin'){ - module.exports = require('./drivers.darwin'); // eslint-disable-line global-require -} else { - module.exports = require('./drivers.win32'); // eslint-disable-line global-require -} diff --git a/app/utils/drivers.win32.js b/app/utils/drivers.win32.js deleted file mode 100644 index cdaef0c2e6..0000000000 --- a/app/utils/drivers.win32.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * == BSD2 LICENSE == - * Copyright (c) 2015-2016, Tidepool Project - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the associated License, which is identical to the BSD 2-Clause - * License as published by the Open Source Initiative at opensource.org. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the License for more details. - * - * You should have received a copy of the License along with this program; if - * not, you can obtain one from Tidepool Project at tidepool.org. - * == BSD2 LICENSE == - */ - -export function checkVersion(dispatch) { - console.log('Not checking driver version on Windows'); -} diff --git a/app/utils/ipc.js b/app/utils/ipc.js new file mode 100644 index 0000000000..aa3e1cf2d1 --- /dev/null +++ b/app/utils/ipc.js @@ -0,0 +1,13 @@ +import _ from 'lodash'; +import env from './env'; + +let ipcRenderer = { send: _.noop, on: _.noop }; +let ipcMain = { send: _.noop, on: _.noop }; + +if (env.electron) { + let electron = require('electron'); + ipcRenderer = electron.ipcRenderer; + ipcMain = electron.ipcMain; +} + +export { ipcRenderer, ipcMain }; diff --git a/app/utils/uploadDataPeriod.js b/app/utils/uploadDataPeriod.js index 46f12a1306..e4b387d842 100644 --- a/app/utils/uploadDataPeriod.js +++ b/app/utils/uploadDataPeriod.js @@ -14,9 +14,8 @@ * not, you can obtain one from Tidepool Project at tidepool.org. * == BSD2 LICENSE == */ -const isElectron = require('is-electron'); -const { ipcRenderer, ipcMain } = require('electron'); -let isRenderer = (process && process.type === 'renderer'); +import env from '../../app/utils/env'; +import { ipcRenderer, ipcMain } from '../../app/utils/ipc'; const PERIODS = { ALL: 1, @@ -24,9 +23,10 @@ const PERIODS = { FOUR_WEEKS: 3, }; -if (isRenderer) { +let uploadDataPeriod; +if (env.electron_renderer) { const remote = require('@electron/remote'); - const uploadDataPeriod = module.exports = { + uploadDataPeriod = module.exports = { get periodGlobal() { return remote.getGlobal('period'); }, @@ -64,49 +64,76 @@ if (isRenderer) { ipcRenderer.on('savePeriodMedtronic600', (event, arg) => { localStorage.setItem('uploadDataPeriodMedtronic600', arg); }); +} -} else { +if (env.electron_main) { // main process + global.periodMedtronic600 = PERIODS.DELTA; + global.period = PERIODS.DELTA; - if (isElectron()) { - global.periodMedtronic600 = PERIODS.DELTA; - global.period = PERIODS.DELTA; + ipcMain.on('setUploadDataPeriodMedtronic600', (event, arg) => { + global.periodMedtronic600 = arg; + }); + ipcMain.on('setUploadDataPeriodGlobal', (event, arg) => { + global.period = arg; + }); - ipcMain.on('setUploadDataPeriodMedtronic600', (event, arg) => { - global.periodMedtronic600 = arg; - }); - ipcMain.on('setUploadDataPeriodGlobal', (event, arg) => { - global.period = arg; - }); + uploadDataPeriod = module.exports = { + get periodGlobal() { + return global.period; + }, + get periodMedtronic600() { + return global.periodMedtronic600; + }, + PERIODS, + // since the main process does not have access to localStorage, + // we have to send the values back to the renderer process to save it + setPeriodMedtronic600: function(toPeriod, window) { + global.periodMedtronic600 = toPeriod; + window.webContents.send('savePeriodMedtronic600', toPeriod); + return global.periodMedtronic600; + }, + setPeriodGlobal: function(toPeriod, window) { + global.period = toPeriod; + window.webContents.send('savePeriodGlobal', toPeriod); + return global.period; + } + }; +} + +if (env.browser) { + let period = PERIODS.DELTA; + let periodMedtronic600 = PERIODS.DELTA; + + uploadDataPeriod = module.exports = { + get periodGlobal() { + return period; + }, + get periodMedtronic600() { + return periodMedtronic600; + }, + PERIODS, + setPeriodMedtronic600: function(toPeriod) { + periodMedtronic600 = toPeriod; + localStorage.setItem('uploadDataPeriodMedtronic600', toPeriod); + return periodMedtronic600; + }, + setPeriodGlobal: function(toPeriod) { + period = toPeriod; + localStorage.setItem('uploadDataPeriodGlobal', toPeriod); + return period; + } + }; +} - const uploadDataPeriod = module.exports = { - get periodGlobal() { - return global.period; - }, - get periodMedtronic600() { - return global.periodMedtronic600; - }, - PERIODS, - // since the main process does not have access to localStorage, - // we have to send the values back to the renderer process to save it - setPeriodMedtronic600: function(toPeriod, window) { - global.periodMedtronic600 = toPeriod; - window.webContents.send('savePeriodMedtronic600', toPeriod); - return global.periodMedtronic600; - }, - setPeriodGlobal: function(toPeriod, window) { - global.period = toPeriod; - window.webContents.send('savePeriodGlobal', toPeriod); - return global.period; - } - }; - } else { - // we're running as a Node process (e.g. running as a script), - // so just default to delta - const uploadDataPeriod = module.exports = { - periodMedtronic600: PERIODS.DELTA, - periodGlobal: PERIODS.DELTA, - PERIODS, - }; - } +if (!env.electron && env.node) { + // we're running as a Node process (e.g. running as a script), + // so just default to delta + uploadDataPeriod = module.exports = { + periodMedtronic600: PERIODS.DELTA, + periodGlobal: PERIODS.DELTA, + PERIODS, + }; } + +export default uploadDataPeriod; diff --git a/app/web.ejs b/app/web.ejs new file mode 100644 index 0000000000..508132e2b2 --- /dev/null +++ b/app/web.ejs @@ -0,0 +1,10 @@ + + + + + Tidepool Uploader + + +
+ + diff --git a/artifact.sh b/artifact.sh new file mode 100755 index 0000000000..39ef9df863 --- /dev/null +++ b/artifact.sh @@ -0,0 +1,7 @@ +#!/bin/sh -e + +wget -q -O artifact_node.sh 'https://raw.githubusercontent.com/tidepool-org/tools/add-circleci/artifact/artifact.sh' +chmod +x artifact_node.sh + +. ./version.sh +./artifact_node.sh node diff --git a/config.server.js b/config.server.js new file mode 100644 index 0000000000..8e13ccba82 --- /dev/null +++ b/config.server.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2014, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + */ + +var fs = require('fs'); + +function maybeReplaceWithContentsOfFile(obj, field) { + var potentialFile = obj[field]; + if (potentialFile != null && fs.existsSync(potentialFile)) { + obj[field] = fs.readFileSync(potentialFile).toString(); + } +} + +var config = {}; + +config.httpPort = process.env.PORT; + +config.httpsPort = process.env.HTTPS_PORT; + +// The https config to pass along to https.createServer. +var theConfig = process.env.HTTPS_CONFIG; +config.httpsConfig = null; +if (theConfig) { + config.httpsConfig = JSON.parse(theConfig); + maybeReplaceWithContentsOfFile(config.httpsConfig, 'key'); + maybeReplaceWithContentsOfFile(config.httpsConfig, 'cert'); + maybeReplaceWithContentsOfFile(config.httpsConfig, 'pfx'); +} + +// Make sure we have an HTTPS config if a port is set +if (config.httpsPort && !config.httpsConfig) { + throw new Error('No https config provided, please set HTTPS_CONFIG with at least the certificate to use.'); +} + +module.exports = config; diff --git a/config/local.example.js b/config/local.example.js new file mode 100644 index 0000000000..1a994b87a5 --- /dev/null +++ b/config/local.example.js @@ -0,0 +1,55 @@ +/** + * Copy this file to `config/local.js` and update as needed + */ + +const linkedPackages = { + // 'tidepool-platform-client': process.env.TIDEPOOL_DOCKER_PLATFORM_CLIENT_DIR || '../platform-client', +}; + +const environments = { + local: { + API_URL: 'http://localhost:8009', + UPLOAD_URL: 'http://localhost:8009', + DATA_URL: 'http://localhost:9220', + BLIP_URL: 'http://localhost:3000' + }, + qa1: { + API_URL: 'https://qa1.development.tidepool.org', + UPLOAD_URL: 'https://qa1.development.tidepool.org', + DATA_URL: 'https://qa1.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa1.development.tidepool.org' + }, + qa2: { + API_URL: 'https://qa2.development.tidepool.org', + UPLOAD_URL: 'https://qa2.development.tidepool.org', + DATA_URL: 'https://qa2.development.tidepool.org/dataservices', + BLIP_URL: 'https://qa2.development.tidepool.org' + }, + int: { + API_URL: 'https://external.integration.tidepool.org/', + UPLOAD_URL: 'https://external.integration.tidepool.org/', + DATA_URL: 'https://external.integration.tidepool.org/dataservices', + BLIP_URL: 'https://external.integration.tidepool.org/' + }, + prd: { + API_URL: 'https://api.tidepool.org', + UPLOAD_URL: 'https://api.tidepool.org', + DATA_URL: 'https://api.tidepool.org/dataservices', + BLIP_URL: 'https://app.tidepool.org' + }, +}; + +// Select environment here +const env = 'qa1'; + +const selectedEnv = environments[env]; +const apiHost = selectedEnv.API_URL; +const uploadApi = apiHost; + +module.exports = { + listLinkedPackages: () => console.log(Object.keys(linkedPackages).join(',')), + linkedPackages, + apiHost, + uploadApi, + environment: selectedEnv +}; diff --git a/docker-compose.yaml b/docker-compose.yaml index 6dff61bfdf..3fb1cf0490 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,10 +6,10 @@ services: privileged: true build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile.dev image: "tidepool/uploader:latest" container_name: "uploader" - environment: + environment: - API_URL=${API_URL:-http://localhost:3000} - UPLOAD_URL=${UPLOAD_URL:-http://localhost:3000} - DATA_URL=${DATA_URL:-http://localhost:3000} @@ -19,7 +19,7 @@ services: - REDUX_DEV_UI=${REDUX_DEV_UI:-false} - DISPLAY=unix$DISPLAY - ROLLBAR_POST_TOKEN=${ROLLBAR_POST_TOKEN} - volumes: + volumes: - .:/home/node/uploader - /tmp/.X11-unix:/tmp/.X11-unix - /dev:/dev diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000000..02b9ee7520 --- /dev/null +++ b/extension/README.md @@ -0,0 +1,7 @@ +# Uploader Helper extension + +This extension supports additional upload mechanisms for specific blood glucose meters, expanding support for compatible devices. + +To distribute a new version of this extension, create a .zip file of this directory and upload the package to the Tidepool Uploader Helper store listing at: + +https://chrome.google.com/u/1/webstore/devconsole/90b45b27-1051-413d-ba60-4b1e3971fe7a/nejgoemnddedidafdoppamlbijokiahb/edit \ No newline at end of file diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000000..4d2885f11e --- /dev/null +++ b/extension/background.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2024, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + */ + +let reply = undefined; +let port = undefined; + +/* global chrome */ + +function connect() { + const hostName = 'org.tidepool.uploader_helper'; + console.log('Connecting to native messaging host', hostName); + port = chrome.runtime.connectNative(hostName); + port.onMessage.addListener(onNativeMessage); + port.onDisconnect.addListener(onDisconnected); +} + +function sendNativeMessage(message) { + port.postMessage(message); + console.log('Sent message:', JSON.stringify(message)); +} + +function onNativeMessage(message) { + if (message.msgType == 'info') { + console.log(message.details); + } else { + reply(message); + } +} + +function onDisconnected() { + console.log('Disconnected: ' + chrome.runtime.lastError.message); + port = null; + reply({ msgType: 'error', details: 'Disconnected: ' + chrome.runtime.lastError.message }); +} + +chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) { + console.log('Received message from the web page:', request); + + if (!port) { + connect(); + } + + if (port) { + sendNativeMessage(request); + + reply = sendResponse; + return true; // indicates we will asynchronously use sendResponse + } else { + sendResponse({ msgType: 'error', details: 'Not connected.'}); + } + } +); diff --git a/extension/icon-128.png b/extension/icon-128.png new file mode 100644 index 0000000000..dd2cf75b40 Binary files /dev/null and b/extension/icon-128.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000000..0d5b5d06e6 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,22 @@ +{ + "manifest_version": 3, + "name": "Tidepool Uploader Helper", + "version": "1.0", + "description": "This extension supports additional upload mechanisms for specific blood glucose meters, expanding support for compatible devices.", + "icons": { + "128": "icon-128.png" + }, + "permissions": [ + "nativeMessaging" + ], + "background": { + "service_worker": "background.js" + }, + "externally_connectable": { + "matches": [ + "http://localhost:3005/*", + "https://*.tidepool.org/*" + ] + }, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAos4V4S/7tWoEGVppnpQaDsl0DjibWk12sSUfr3chmbDcPfQPw+1iZNZ16oMazdfyj/UBQA5hYlCycSSGExvl8N1JM0/sJ5DEs1hy5cmyR6MuAdXOuu0X1NzRS8rdi67msWXVJUO51AUIByHy5BArjw3AzaE4kxAWRexUvMjb8wC7/k5hmU7pimL/t7sa0O6ghmzJFvqu7nzK6x5+bGeHRfXg0OLqtAc3eLByRZbT0HOfgPEFsBq5LrbS3/MvzjNorWon0BcLYV7JayFkmpV/p3nQp+7SwiHKadsexlwreOly+Gk6tppkieiYsznI4kqQOpVrPrI+iMe+LCfw4NzhVQIDAQAB" +} diff --git a/lib/core/api.js b/lib/core/api.js index db93bbc88b..36ccdaaad9 100644 --- a/lib/core/api.js +++ b/lib/core/api.js @@ -17,10 +17,8 @@ import _ from 'lodash'; import async from 'async'; import { format } from 'util'; -import crypto from 'crypto'; import sundial from 'sundial'; import { v4 as uuidv4 } from 'uuid'; -import isElectron from 'is-electron'; import os from 'os'; import bows from 'bows'; // Wrapper around the Tidepool client library @@ -32,9 +30,10 @@ import localStore from './localStore'; import rollbar from '../../app/utils/rollbar'; import * as actionUtils from '../../app/actions/utils'; import personUtils from './personUtils'; +import env from '../../app/utils/env'; // eslint-disable-next-line no-console -const log = isElectron() ? bows('Api') : console.log; +const log = env.electron ? bows('Api') : console.log; // for cli tools running in node if (typeof localStore === 'function') { @@ -52,7 +51,7 @@ const api = { // synchronous! api.create = (options) => { // eslint-disable-next-line no-console - const tidepoolLog = isElectron() ? bows('Tidepool') : console.log; + const tidepoolLog = env.node ? console.log : bows('Tidepool'); tidepool = createTidepoolClient({ host: options.apiUrl, uploadApi: options.uploadUrl, @@ -680,6 +679,17 @@ api.upload.toPlatform = (data, sessionInfo, progress, groupId, cb, uploadType = async.waterfall([ (callback) => { + // generate digest + const encoder = new TextEncoder(); + const data = encoder.encode(`${sessionInfo.deviceId}_${sessionInfo.start}`); + crypto.subtle.digest('SHA-256', data).then((digest) => { + const hex = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join(''); + return hex; + }).then((hexDigest) => { + callback(null, hexDigest); + }); + }, + (digest, callback) => { // generate and post the upload metadata const now = new Date(); @@ -741,16 +751,11 @@ api.upload.toPlatform = (data, sessionInfo, progress, groupId, cb, uploadType = _.set(uploadItem, 'client.private.os', `${os.platform()}-${os.arch()}-${os.release()}`); if (uploadType === 'jellyfish') { - uploadItem.with_uploadId(`upid_${ - crypto.createHash('md5') - .update(`${sessionInfo.deviceId}_${sessionInfo.start}`) - .digest('hex') - .slice(0, 12)}`); + uploadItem.with_uploadId(`upid_${digest.slice(0, 12)}`); uploadItem.with_guid(uuidv4()); uploadItem.with_byUser(tidepool.getUserId()); } uploadItem = uploadItem.done(); - api.log('create dataset'); if (uploadType === 'dataservices') { @@ -810,6 +815,9 @@ api.upload.toPlatform = (data, sessionInfo, progress, groupId, cb, uploadType = ], (err, result) => { if (err == null) { api.log('upload.toPlatform: all good'); + if (env.browser && rollbar) { + rollbar.info('Successful Uploader-in-Web upload', sessionInfo); + } return cb(null, result); } api.log('upload.toPlatform: failed ', err); @@ -838,13 +846,11 @@ api.getMostRecentUploadRecord = (userId, deviceId, cb) => { }); }; -api.upload.blob = (blob, contentType, cb) => { +api.upload.blob = async (blob, contentType, cb) => { api.log('POST /blobs'); - - const digest = crypto.createHash('md5').update(blob).digest('base64'); const blobObject = new Blob([blob], { type: contentType }); - tidepool.uploadBlobForUser(tidepool.getUserId(), blobObject, contentType, `MD5=${digest}`, (err, result) => { + tidepool.uploadBlobForUser(tidepool.getUserId(), blobObject, contentType, null, (err, result) => { if (err) { return cb(err, null); } diff --git a/lib/core/device.js b/lib/core/device.js index 9469fd4dd0..a2533b8f13 100644 --- a/lib/core/device.js +++ b/lib/core/device.js @@ -18,12 +18,8 @@ import os from 'os'; import _ from 'lodash'; import async from 'async'; -import util from 'util'; import bows from 'bows'; -/* eslint-disable import/no-extraneous-dependencies */ -import { getDeviceList, findByIds, webusb } from 'usb'; - import debugMode from '../../app/utils/debugMode'; import serialDevice from '../serialDevice'; import hidDevice from '../hidDevice'; @@ -32,6 +28,8 @@ import bleDevice from '../bleDevice'; import driverManager from '../driverManager'; import builder from '../objectBuilder'; import driverManifests from './driverManifests'; +import { ipcRenderer } from '../../app/utils/ipc'; +import env from '../../app/utils/env'; import dexcomDriver from '../drivers/dexcom/dexcomDriver'; import oneTouchUltraMini from '../drivers/onetouch/oneTouchUltraMini'; @@ -115,6 +113,7 @@ device.deviceComms = { Dexcom: serialDevice, OneTouchUltraMini: serialDevice, AbbottPrecisionXtra: serialDevice, + InsuletOmniPod: usbDevice, OneTouchUltra2: serialDevice, OneTouchVerio: usbDevice, OneTouchVerioIQ: serialDevice, @@ -241,62 +240,6 @@ device.createDriverConfig = (driverId, options = {}) => { }; }; -device.findUsbDevice = (driverId, usbDevices) => { - let userSpaceDriver = null; - const driverManifest = device.getDriverManifest(driverId); - const combos = _.map(usbDevices, (i) => _.pick(i, 'product', 'vendorId', 'productId')); - - device.log('Looking for USB PID/VID(s): ', JSON.stringify(driverManifest.usb)); - device.log('Available USB PID/VIDs:', JSON.stringify(combos)); - - for (let i = 0; i < driverManifest.usb.length; i++) { - device.log('USB details for ', JSON.stringify(driverManifest.usb[i]), ':', - util.inspect(findByIds( - driverManifest.usb[i].vendorId, - driverManifest.usb[i].productId, - ))); - } - - const matchingUsbDevices = _.filter(usbDevices, (matching) => { - let found = false; - for (let i = 0; i < driverManifest.usb.length; i++) { - if (driverManifest.usb[i].vendorId === matching.vendorId - && driverManifest.usb[i].productId === matching.productId) { - userSpaceDriver = driverManifest.usb[i].driver; - found = true; - } - } - return found; - }); - - const devices = _.map(matchingUsbDevices, (result) => ({ - driverId, - deviceId: result.deviceId, - vendorId: result.vendorId, - productId: result.productId, - userSpaceDriver, - bitrate: driverManifest.bitrate, - stopBits: driverManifest.stopBits, - })); - - if (devices.length > 1) { - device.log(`WARNING: More than one device found for ${driverId}`); - device.othersConnected = devices.length - 1; - } - - return _.first(devices); -}; - -device.detectUsb = (driverId, cb) => { - const usbDevices = _.map(getDeviceList(), (result) => ({ - deviceId: result.deviceDescriptor.idDevice, - vendorId: result.deviceDescriptor.idVendor, - productId: result.deviceDescriptor.idProduct, - })); - - return cb(null, device.findUsbDevice(driverId, usbDevices)); -}; - // eslint-disable-next-line consistent-return device.detect = (driverId, options, cb) => { if (_.isFunction(options)) { @@ -307,10 +250,22 @@ device.detect = (driverId, options, cb) => { } const driverManifest = device.getDriverManifest(driverId); + if (options.filename) { + // we're dealing with an Eros PDM + return cb(); + } + if (driverManifest.mode === 'block' && driverId === 'AbbottLibreView') { return cb(); } + if (env.browser) { + // in the browser we handle these with an extension + if (driverId === 'OneTouchSelect' || driverId === 'OneTouchVerio') { + return cb(null, { driverId }); + } + } + if (options.hidDevice) { // We've got a Web HID device! const devdata = { @@ -341,7 +296,7 @@ device.detect = (driverId, options, cb) => { let devdata = null; (async () => { - const existingPermissions = await webusb.getDevices(); + const existingPermissions = await navigator.usb.getDevices(); for (let i = 0; i < existingPermissions.length; i++) { for (let j = 0; j < driverManifest.usb.length; j++) { @@ -360,7 +315,8 @@ device.detect = (driverId, options, cb) => { }; if (webUSBDevice == null) { - webUSBDevice = await webusb.requestDevice({ filters }); + ipcRenderer.send('setUSBFilter', filters); + webUSBDevice = await navigator.usb.requestDevice({ filters }); } if (webUSBDevice == null) { @@ -446,26 +402,63 @@ device.detect = (driverId, options, cb) => { } else { // no matching devices were found, let's see if they are // actually connected via USB - device.detectUsb(driverId, (error, devdata2) => { - if (error) { - return cb(error); + + const filters = driverManifest.usb.map(({ vendorId, productId }) => ({ + vendorId, + productId, + })); + let webUSBDevice = null; + let userSpaceDriver = null; + + (async () => { + const existingPermissions = await navigator.usb.getDevices(); + + for (let i = 0; i < existingPermissions.length; i++) { + for (let j = 0; j < driverManifest.usb.length; j++) { + if (driverManifest.usb[j].vendorId === existingPermissions[i].vendorId + && driverManifest.usb[j].productId === existingPermissions[i].productId) { + device.log('Device has already been granted permission'); + webUSBDevice = existingPermissions[i]; + } + } } - if (!devdata2) { - return cb(); + + if (webUSBDevice == null) { + ipcRenderer.send('setUSBFilter', filters); + webUSBDevice = await navigator.usb.requestDevice({ filters }); } - device.deviceInfoCache[driverId] = _.cloneDeep(devdata2); - // hey, we can see it on the USB bus! - // let's try the userspace driver if available - if (devdata2.userSpaceDriver) { - return device.detectHelper(driverId, options, (error2, userspaceDevice) => { - if (error2) { - return cb(error2); - } - _.assign(device, userspaceDevice); - return cb(null, devdata2); - }); + + if (webUSBDevice == null) { + throw new Error('No device was selected.'); } - return cb(null, devdata2); + + for (let j = 0; j < driverManifest.usb.length; j++) { + if (driverManifest.usb[j].vendorId === webUSBDevice.vendorId + && driverManifest.usb[j].productId === webUSBDevice.productId) { + userSpaceDriver = driverManifest.usb[j].driver; + } + } + + const devdata = { + driverId, + usbDevice: webUSBDevice, + userSpaceDriver, + }; + + if (driverManifest.bitrate) { + devdata.bitrate = driverManifest.bitrate; + } + + device.deviceInfoCache[driverId] = _.cloneDeep(devdata); + device.detectHelper(driverId, options, (err) => { + if (err) { + return cb(err); + } + return cb(null, devdata); + }); + })().catch((error) => { + device.log('WebUSB error:', error); + return cb(error); }); } }; diff --git a/lib/core/localStore.js b/lib/core/localStore.js index ba0231c04d..186cbc7367 100644 --- a/lib/core/localStore.js +++ b/lib/core/localStore.js @@ -15,7 +15,8 @@ * == BSD2 LICENSE == */ -const isElectron = require('is-electron'); +import env from '../../app/utils/env'; + const storage = require('./storage'); const mockStore = { @@ -27,7 +28,7 @@ const mockStore = { let localStore = null; let ourStore = null; -if (isElectron()) { +if (env.electron_renderer || env.browser) { ourStore = typeof window !== 'undefined' ? window.localStorage : mockStore; } diff --git a/lib/drivers/bluetoothLE/bluetoothLEDriver.js b/lib/drivers/bluetoothLE/bluetoothLEDriver.js index cc9c4d1e65..cf0e4be161 100644 --- a/lib/drivers/bluetoothLE/bluetoothLEDriver.js +++ b/lib/drivers/bluetoothLE/bluetoothLEDriver.js @@ -27,8 +27,8 @@ import common from '../../commonFunctions'; const struct = structJs(); let remote; -if (env.electron) { - remote = require('@electron/remote'); +if(env.electron){ + remote = require('@electron/remote'); } const isBrowser = typeof window !== 'undefined'; @@ -105,7 +105,13 @@ module.exports = (config) => { } cfg.deviceTags = ['bgm']; - cfg.deviceInfo.deviceId = `${[cfg.deviceInfo.manufacturers]}-${cfg.deviceInfo.model}-${remote.getGlobal('bluetoothDeviceId')}`; + cfg.deviceInfo.deviceId = `${[cfg.deviceInfo.manufacturers]}-${cfg.deviceInfo.model}`; + if(env.electron){ + cfg.deviceInfo.deviceId += `-${remote.getGlobal('bluetoothDeviceId')}`; + } else { + cfg.deviceInfo.deviceId += `-${cfg.deviceComms.ble.device.id}`; + } + cfg.builder.setDefaults({ deviceId: cfg.deviceInfo.deviceId }); if (cfg.deviceInfo.name.startsWith('CareSens') || @@ -181,12 +187,9 @@ module.exports = (config) => { debug('Error in time sync: ', error); return cb(error, null); }); - } else { return cb(null, data); } - - }).catch((error) => { debug('Error in getConfigInfo: ', error); return cb(error, null); diff --git a/lib/drivers/insulet/insuletDriver.js b/lib/drivers/insulet/insuletDriver.js index 5aae53dee3..833db02bf1 100644 --- a/lib/drivers/insulet/insuletDriver.js +++ b/lib/drivers/insulet/insuletDriver.js @@ -1787,39 +1787,41 @@ module.exports = (config) => { // eslint-disable-next-line consistent-return (async () => { try { - const drivelist = require('drivelist'); - const drives = await drivelist.list(); - - let filePath = null; - debug('Drives:', drives); - // eslint-disable-next-line no-restricted-syntax - for (const drive of drives) { - debug(drive.description); - if (drive.description && drive.description.toLowerCase().includes('omnipod') && !drive.system) { - if (drive.mountpoints.length > 0) { - const devicePath = drive.mountpoints[0].path; - // eslint-disable-next-line no-await-in-loop - const contents = await fs.readdir(devicePath); - const files = _.filter(contents, (file) => { - const regex = /(.+\.ibf)/g; - return file.match(regex); - }); - // On Eros PDM there should only be one .ibf file - filePath = path.join(devicePath, files[0]); - } else { - return cb(new Error('File not found. Please unplug PDM and try again.'), null); + if (is.electron) { + const drivelist = require('drivelist'); + const drives = await drivelist.list(); + + let filePath = null; + debug('Drives:', drives); + // eslint-disable-next-line no-restricted-syntax + for (const drive of drives) { + debug(drive.description); + if (drive.description && drive.description.toLowerCase().includes('omnipod') && !drive.system) { + if (drive.mountpoints.length > 0) { + const devicePath = drive.mountpoints[0].path; + // eslint-disable-next-line no-await-in-loop + const contents = await fs.readdir(devicePath); + const files = _.filter(contents, (file) => { + const regex = /(.+\.ibf)/g; + return file.match(regex); + }); + // On Eros PDM there should only be one .ibf file + filePath = path.join(devicePath, files[0]); + } else { + return cb(new Error('File not found. Please unplug PDM and try again.'), null); + } } } - } - if (filePath) { - debug('filePath:', filePath); + if (filePath) { + debug('filePath:', filePath); - const res = await fs.readFile(filePath); - buf = res.buffer; - bytes = new Uint8Array(buf); - progress(100); - return cb(null, data); + const res = await fs.readFile(filePath); + buf = res.buffer; + bytes = new Uint8Array(buf); + progress(100); + return cb(null, data); + } } // no mounted drive with .ibf file, let's try connecting over MTP (Dash) diff --git a/lib/drivers/onetouch/oneTouchVerio.js b/lib/drivers/onetouch/oneTouchVerio.js index 0aec27e983..b00eaff361 100644 --- a/lib/drivers/onetouch/oneTouchVerio.js +++ b/lib/drivers/onetouch/oneTouchVerio.js @@ -16,6 +16,7 @@ */ /* eslint-disable max-classes-per-file */ +/* global chrome */ import fs from 'fs'; import { assign, clone, invert, trimEnd } from 'lodash'; @@ -25,10 +26,10 @@ import TZOUtil from '../../TimezoneOffsetUtil'; import annotate from '../../eventAnnotations'; import crc from '../../crc'; import common from '../../commonFunctions'; -import { sudo as catalinaSudo } from './catalina-sudo/sudo'; import semver from 'semver'; import os from 'os'; import { usb, findByIds } from 'usb'; +import env from '../../../app/utils/env'; const isBrowser = typeof window !== 'undefined'; // eslint-disable-next-line no-console @@ -304,8 +305,10 @@ class BlockDevice { if (os.platform() === 'darwin' && semver.compare(os.release(), '19.0.0') >= 0) { // >= macOS Catalina + // eslint-disable-next-line global-require + const { sudo } = require('./catalina-sudo/sudo'); const cmd = `chmod a+rw "${this.devicePath}"`; - const result = await catalinaSudo(cmd); + const result = await sudo(cmd); debug('Result of sudo command:', result); } @@ -395,7 +398,10 @@ class BlockDevice { } callback(error); } else { - const readBuffer = directIO.getAlignedBuffer(BLOCKDEVICE_BLOCKSIZE, 4096); + let readBuffer = Buffer.alloc(USB_BULK_BLOCKSIZE); + if (!env.browser) { + readBuffer = directIO.getAlignedBuffer(USB_BULK_BLOCKSIZE, 4096); + } fs.read( this.fileHandle, readBuffer, 0, BLOCKDEVICE_BLOCKSIZE, seekOffset, (err2) => { @@ -414,11 +420,77 @@ class BlockDevice { } } +class NativeMessaging { + constructor() { + this.extensionId = 'nejgoemnddedidafdoppamlbijokiahb'; + } + + // eslint-disable-next-line consistent-return + openDevice(deviceInfo, callback) { + if (!chrome.runtime) { + return callback(new Error('Uploader Helper extension not installed.')); + } + + chrome.runtime.sendMessage(this.extensionId, { command: 'getAppVersion' }, (version) => { + debug('App version:', version?.details); + + chrome.runtime.sendMessage(this.extensionId, { command: 'openDevice' }, (response) => { + debug('Response from extension:', response); + if (response.msgType === 'error') { + callback(new Error(response.details)); + } else { + callback(null); + } + }); + }); + } + + closeDevice(callback) { + chrome.runtime.sendMessage(this.extensionId, { command: 'closeDevice' }, (response) => { + debug('Closed:', response); + if (callback) { + return callback(); + } + return null; + }); + } + + checkDevice(callback) { + chrome.runtime.sendMessage(this.extensionId, { command: 'checkDevice' }, (response) => { + debug('Response:', response); + + if (response.msgType === 'data') { + // make very sure this is the device we are looking for + if (response.details !== BLOCKDEVICE_SIGNATURE) { + this.closeDevice(); + return callback(new Error('Did not find device signature')); + } + return callback(null); + } + return callback(new Error(response.details)); + }); + } + + retrieveData(lbaNumber, requestData, callback) { + const seekOffset = lbaNumber * BLOCKDEVICE_BLOCKSIZE; + + chrome.runtime.sendMessage(this.extensionId, { command: 'retrieveData', request: Array.from(requestData), seekOffset }, (response) => { + if (response.msgType === 'data') { + callback(null, Buffer.from(response.details)); + } else { + callback(new Error(response.details)); + } + }); + } +} + class OneTouchVerio { constructor(cfg) { this.cfg = cfg; if (process.platform === 'linux') { this.communication = new USBScsiDevice(); + } else if (env.browser) { + this.communication = new NativeMessaging(); } else { this.communication = new BlockDevice(); } @@ -497,7 +569,10 @@ class OneTouchVerio { } retrieveQueryData(queryType, callback) { - const linkRequestData = directIO.getAlignedBuffer(USB_BULK_BLOCKSIZE, 4096); + const linkRequestData = Buffer.alloc(USB_BULK_BLOCKSIZE); + if (!env.browser) { + directIO.getAlignedBuffer(USB_BULK_BLOCKSIZE, 4096); + } const applicationData = Buffer.from([SERVICE_ID, 0xe6, 0x02, queryType]); this.constructor.buildLinkLayerFrame(applicationData).copy(linkRequestData); this.retrieveData(LBA_NUMBER.GENERAL, linkRequestData, (err, commandData) => { @@ -570,7 +645,7 @@ class OneTouchVerio { if (err) { return callback(err); } - const recordCount = commandData.readUInt16LE(); + const recordCount = commandData.readUInt16LE(0); debug('retrieveRecordCount:', recordCount); const data = {}; data.recordCount = recordCount; @@ -665,6 +740,7 @@ export default function (config) { // With no date & time settings changes available, // timezone is applied across-the-board cfg.tzoUtil = new TZOUtil(cfg.timezone, new Date().toISOString(), []); + cfg.deviceInfo = {}; assign(cfg.deviceInfo, { tags: ['bgm'], manufacturers: ['LifeScan'], @@ -680,7 +756,7 @@ export default function (config) { setup(deviceInfo, progress, cb) { progress(100); - cb(null, { deviceInfo }); + return cb(null, { deviceInfo }); }, connect(progress, data, cb) { @@ -720,6 +796,7 @@ export default function (config) { cb(err2, null); return; } + assign(cfg.deviceInfo, { model: deviceModel.deviceModel }); data.deviceModel = cfg.deviceInfo.model; // for metrics progress(100); @@ -804,7 +881,7 @@ export default function (config) { deviceManufacturers: cfg.deviceInfo.manufacturers, deviceModel: cfg.deviceInfo.model, deviceSerialNumber: cfg.deviceInfo.serialNumber, - deviceTime: data.deviceInfo.deviceTime, + deviceTime: cfg.deviceInfo.deviceTime, deviceId: cfg.deviceInfo.deviceId, start: sundial.utcDateString(), timeProcessing: cfg.tzoUtil.type, diff --git a/lib/drivers/onetouch/oneTouchVerioBLE.js b/lib/drivers/onetouch/oneTouchVerioBLE.js index 08699689e4..92891626e6 100644 --- a/lib/drivers/onetouch/oneTouchVerioBLE.js +++ b/lib/drivers/onetouch/oneTouchVerioBLE.js @@ -459,7 +459,12 @@ export default (config) => { _.assign(cfg.deviceInfo, await cfg.deviceComms.ble.getDeviceInfo()); })().then(() => { cfg.deviceTags = ['bgm']; - cfg.deviceInfo.deviceId = `${[cfg.deviceInfo.manufacturers]}-${cfg.deviceInfo.model.replace(/\s+/g, '')}-${remote.getGlobal('bluetoothDeviceId')}`; + cfg.deviceInfo.deviceId = `${[cfg.deviceInfo.manufacturers]}-${cfg.deviceInfo.model.replace(/\s+/g, '')}`; + if (env.electron) { + cfg.deviceInfo.deviceId += `-${remote.getGlobal('bluetoothDeviceId')}`; + } else { + cfg.deviceInfo.deviceId += `-${cfg.deviceComms.ble.device.id}`; + } data.deviceModel = cfg.deviceInfo.model; // for metrics cfg.builder.setDefaults({ deviceId: cfg.deviceInfo.deviceId }); progress(100); diff --git a/lib/drivers/roche/accuChekUSB.js b/lib/drivers/roche/accuChekUSB.js index 0636b1e167..87cb3383ab 100644 --- a/lib/drivers/roche/accuChekUSB.js +++ b/lib/drivers/roche/accuChekUSB.js @@ -18,7 +18,6 @@ import _ from 'lodash'; import sundial from 'sundial'; /* eslint-disable import/no-extraneous-dependencies */ -import { webusb } from 'usb'; import annotate from '../../eventAnnotations'; import common from '../../commonFunctions'; @@ -51,7 +50,7 @@ class AccuChekUSB { // eslint-disable-next-line consistent-return async openDevice(deviceInfo, cb) { - const devices = await webusb.getDevices(); + const devices = await navigator.usb.getDevices(); // eslint-disable-next-line no-restricted-syntax for (const usbDevice of devices) { diff --git a/lib/drivers/weitai/weiTaiUSB.js b/lib/drivers/weitai/weiTaiUSB.js index ebe2dd88fb..222b6a3020 100644 --- a/lib/drivers/weitai/weiTaiUSB.js +++ b/lib/drivers/weitai/weiTaiUSB.js @@ -18,7 +18,6 @@ import _ from 'lodash'; import sundial from 'sundial'; -import { webusb } from 'usb'; import crypto from 'crypto'; import TZOUtil from '../../TimezoneOffsetUtil'; @@ -32,9 +31,11 @@ import { concatArrayBuffer, } from './utils'; +import { ipcRenderer } from '../../../app/utils/ipc'; +import common from '../../commonFunctions'; + const isBrowser = typeof window !== 'undefined'; const debug = isBrowser ? require('bows')('WeitaiUSBDriver') : console.log; -const common = require('../../commonFunctions'); class WeitaiUSB { constructor(cfg) { @@ -43,14 +44,15 @@ class WeitaiUSB { async openDevice(deviceInfo, cb) { try { - this.usbDevice = await webusb.requestDevice({ - filters: [ - { - vendorId: deviceInfo.usbDevice.vendorId, - productId: deviceInfo.usbDevice.productId, - }, - ], - }); + const devices = await navigator.usb.getDevices(); + + // eslint-disable-next-line no-restricted-syntax + for (const usbDevice of devices) { + if (usbDevice.productId === deviceInfo.usbDevice.productId + && usbDevice.vendorId === deviceInfo.usbDevice.vendorId) { + this.usbDevice = usbDevice; + } + } if (this.usbDevice == null) { return cb(new Error('Could not find device')); @@ -72,7 +74,7 @@ class WeitaiUSB { try { // eslint-disable-next-line prefer-destructuring this.usbDevice.iface = this.usbDevice.configuration.interfaces[3]; - this.usbDevice.claimInterface(this.usbDevice.iface.interfaceNumber); + await this.usbDevice.claimInterface(this.usbDevice.iface.interfaceNumber); } catch (e) { return cb(e, null); } @@ -102,7 +104,9 @@ class WeitaiUSB { value: 0x00, index: 0x00, }; - const buff1 = 'MicrotechMD\0'; + + const enc = new TextEncoder(); + const buff1 = enc.encode('MicrotechMD\0'); await this.usbDevice.controlTransferOut(getStatus1, buff1); const getStatus2 = { @@ -112,7 +116,7 @@ class WeitaiUSB { value: 0x00, index: 0x01, }; - const buff2 = 'Equil\0'; + const buff2 = enc.encode('Equil\0'); await this.usbDevice.controlTransferOut(getStatus2, buff2); const getStatus3 = { @@ -122,7 +126,7 @@ class WeitaiUSB { value: 0x00, index: 0x03, }; - const buff3 = '1.0\0'; + const buff3 = enc.encode('1.0\0'); await this.usbDevice.controlTransferOut(getStatus3, buff3); const getStatus4 = { @@ -133,12 +137,25 @@ class WeitaiUSB { index: 0x00, }; - await this.usbDevice.controlTransferOut(getStatus4, []); + await this.usbDevice.controlTransferOut(getStatus4, new ArrayBuffer(0)); await this.usbDevice.close(); - setTimeout(() => { + setTimeout(async () => { const newDevice = deviceInfo; - newDevice.usbDevice.vendorId = 6353; - newDevice.usbDevice.productId = 11521; + + const filters = [ + { + vendorId: 6353, + productId: 11521, + }, + ]; + + ipcRenderer.send('setUSBFilter', filters); + newDevice.usbDevice = await navigator.usb.requestDevice({ filters }); + + if (newDevice.usbDevice == null) { + return cb(new Error('Could not find device in accessory mode')); + } + this.openDevice(newDevice, cb); }, 3000); } catch (error) { @@ -154,7 +171,7 @@ class WeitaiUSB { async open18d1(cb) { debug('in Accessory Mode!'); [this.usbDevice.iface] = this.usbDevice.configuration.interfaces; - this.usbDevice.claimInterface(this.usbDevice.iface.interfaceNumber); + await this.usbDevice.claimInterface(this.usbDevice.iface.interfaceNumber); [this.inEndpoint, this.outEndpoint] = this.usbDevice.iface.alternate.endpoints; @@ -164,7 +181,7 @@ class WeitaiUSB { static buildSettingPacket(payload, commandBody) { const md5 = crypto .createHash('md5') - .update(new DataView(payload)) + .update(Buffer.from(payload)) .digest(); const commandBodyView = new DataView(commandBody); @@ -211,7 +228,7 @@ class WeitaiUSB { const md5C = crypto .createHash('md5') - .update(new DataView(payload)) + .update(Buffer.from(payload)) .digest(); if (!_.isEqual(new Uint8Array(md5), md5C)) { @@ -302,7 +319,7 @@ class WeitaiUSB { static buildPacket(payload) { const md5 = crypto .createHash('md5') - .update(new DataView(payload)) + .update(Buffer.from(payload)) .digest(); const data = concatArrayBuffer(payload, md5); @@ -353,7 +370,7 @@ class WeitaiUSB { const md5C = crypto .createHash('md5') - .update(new DataView(payload)) + .update(Buffer.from(payload)) .digest(); if (!_.isEqual(new Uint8Array(md5), md5C)) { diff --git a/lib/serialDevice.js b/lib/serialDevice.js index 7bfe47422c..5846aff717 100644 --- a/lib/serialDevice.js +++ b/lib/serialDevice.js @@ -14,9 +14,6 @@ * not, you can obtain one from Tidepool Project at tidepool.org. * == BSD2 LICENSE == */ - -import { findByIds } from 'usb'; - var _ = require('lodash'); var async = require('async'); @@ -220,32 +217,38 @@ module.exports = function(config) { switch(deviceInfo.userSpaceDriver) { case 'pl2303': - connection = new PL2303(connectopts); - connection.on('ready', function () { + connection = new PL2303(deviceInfo.usbDevice, connectopts); + connection.addEventListener('error', function (event) { + return cb(event.detail); + }); + connection.addEventListener('ready', function () { return cb(); }); break; case 'cdc-acm': - var device = findByIds( deviceInfo.vendorId, deviceInfo.productId ); + var device = deviceInfo.usbDevice; device.open(); connection = UsbCdcAcm.fromUsbDevice(device, connectopts); connection.cdcAcm = device; return cb(); break; case 'cp2102': - connection = new CP2102(deviceInfo.vendorId, deviceInfo.productId, connectopts); - connection.on('ready', function () { + connection = new CP2102(deviceInfo.usbDevice, connectopts); + connection.addEventListener('error', function (event) { + return cb(event.detail); + }); + connection.addEventListener('ready', function () { return cb(); }); break; - case 'ftdi': - connection = new FTDI(deviceInfo.vendorId, deviceInfo.productId, connectopts); - connection.addEventListener('error', function (event) { - return cb(event.detail); - }); - connection.addEventListener('ready', function () { - return cb(); - }); + case 'ftdi': + connection = new FTDI(deviceInfo.usbDevice, connectopts); + connection.addEventListener('error', function (event) { + return cb(event.detail); + }); + connection.addEventListener('ready', function () { + return cb(); + }); break; /* TODO: waiting on upstream fix, see https://bugs.chromium.org/p/chromium/issues/detail?id=1189418 case 'tusb3410': @@ -290,16 +293,9 @@ module.exports = function(config) { debug('connected via ' + deviceInfo.userSpaceDriver); // add a listener for any serial traffic - if (deviceInfo.userSpaceDriver === 'ftdi') { - connection.addEventListener('data', (event) => { - portListener(event.detail); - }); - } else { - // CP2102 and PL2303 still uses EventEmitter - connection.on('data', function (data) { - portListener(data); - }); - } + connection.addEventListener('data', (event) => { + portListener(event.detail); + }); } return cb(); @@ -385,10 +381,7 @@ module.exports = function(config) { debug('No connection details available.'); callback(new Error('No connection details available.')); } else { - if (connection.userSpaceDriver === 'pl2303') { - connection.send(Buffer.from(bytes)); - callback(); - } else if (connection.writable && !connection.userSpaceDriver) { + if (connection.writable && !connection.userSpaceDriver) { // hey, we're using the web serial API! (async () => { const writer = connection.writable.getWriter(); diff --git a/lib/usbDevice.js b/lib/usbDevice.js index cb3037391a..0698243dce 100644 --- a/lib/usbDevice.js +++ b/lib/usbDevice.js @@ -15,19 +15,14 @@ * == BSD2 LICENSE == */ -import { webusb } from 'usb'; - -const isBrowser = typeof window !== 'undefined'; -const debug = isBrowser ? require('bows')('usbDevice') : console.log; - -export default class UsbDevice { + export default class UsbDevice { constructor(deviceInfo) { const self = this; self.device = deviceInfo.usbDevice; (async () => { if (self.device == null) { - const devices = await webusb.getDevices(); + const devices = await navigator.usb.getDevices(); for (const usbDevice of devices) { if (usbDevice.productId === deviceInfo.productId && usbDevice.vendorId === deviceInfo.vendorId) { @@ -37,7 +32,7 @@ export default class UsbDevice { } if (self.device == null) { - self.device = await webusb.requestDevice({ + self.device = await navigator.usb.requestDevice({ filters: [ { vendorId: deviceInfo.vendorId, diff --git a/locales/en/translation.missing.json b/locales/en/translation.missing.json index f4ef53f51d..79b3b16c8b 100644 --- a/locales/en/translation.missing.json +++ b/locales/en/translation.missing.json @@ -1,52 +1,20 @@ { + "Plug cable into meter and then connect cable to computer": "Plug cable into meter and then connect cable to computer", "Select CSV file downloaded from LibreView": "Select CSV file downloaded from LibreView", "For uploading instructions,": "For uploading instructions,", "visit our support site": "visit our support site", + "Hold Bluetooth switch on meter until Bluetooth indicator starts to flash": "Hold Bluetooth switch on meter until Bluetooth indicator starts to flash", "Plug in meter with cable and set meter to": "Plug in meter with cable and set meter to", "PC Link Mode": "PC Link Mode", "Make sure the meter is switched off and plug in with micro-USB cable": "Make sure the meter is switched off and plug in with micro-USB cable", "Make sure the meter is switched off and plug in cable": "Make sure the meter is switched off and plug in cable", - "Turn meter on and make sure Bluetooth is switched on": "Turn meter on and make sure Bluetooth is switched on", - "Nexus and HCT: Plug in meter with mini-USB": "Nexus and HCT: Plug in meter with mini-USB", - "Nexus Mini Ultra & Go: Plug in meter with strip port cable": "Nexus Mini Ultra & Go: Plug in meter with strip port cable", "Nexus and HCT: Plug in meter with mini-USB cable": "Nexus and HCT: Plug in meter with mini-USB cable", - "Plug in PDA with micro-USB": "Plug in PDA with micro-USB", - "Hold Bluetooth switch on meter until Bluetooth indicator starts to flash": "Hold Bluetooth switch on meter until Bluetooth indicator starts to flash", - "We couldn't log you in. Try again in a few minutes.": "We couldn't log you in. Try again in a few minutes.", - "Newest version: {{newVersion}} - released {{newDate}}": "Newest version: {{newVersion}} - released {{newDate}}", - "You can continue to use your current version, but we recommend updating as soon as possible.": "You can continue to use your current version, but we recommend updating as soon as possible.", - "Your version: {{currentVersion}} - released {{currentDate}}": "Your version: {{currentVersion}} - released {{currentDate}}", - "See what's new with Tidepool Uploader.": "See what's new with Tidepool Uploader.", - "You can continue to use your current version,
but we recommend updating as soon as possible.": "You can continue to use your current version,
but we recommend updating as soon as possible.", - "You can continue to use your current version,": "You can continue to use your current version,", - "but we recommend updating as soon as possible.": "but we recommend updating as soon as possible.", - "See what's new with Tidepool Uploader": "See what's new with Tidepool Uploader", - "Your version: {{currentVersion}} - installed {{currentDate}}": "Your version: {{currentVersion}} - installed {{currentDate}}", - "Your version: {{currentVersion}} - ": "Your version: {{currentVersion}} - ", - "Couldn't connect to device.": "Couldn't connect to device.", - "Enter Bluetooth Passkey for device {{device}}:": "Enter Bluetooth Passkey for device {{device}}:", + "Nexus Mini Ultra & Go: Plug in meter with strip port cable": "Nexus Mini Ultra & Go: Plug in meter with strip port cable", + "Plug into USB. Wait for Export to complete. Click Upload.": "Plug into USB. Wait for Export to complete. Click Upload.", + "Plug in meter with micro-USB cable and make sure Uploader Helper extension is installed": "Plug in meter with micro-USB cable and make sure Uploader Helper extension is installed", + "Turn meter on and make sure Bluetooth is switched on": "Turn meter on and make sure Bluetooth is switched on", "Make sure meter is switched off before plugging in cable": "Make sure meter is switched off before plugging in cable", - "Do you want to connect to {{device}}?": "Do you want to connect to {{device}}?", - "Confirm": "Confirm", - "clinic": "clinic", - "Want to use Tidepool for your private data?": "Want to use Tidepool for your private data?", - "Go to Private Workspace": "Go to Private Workspace", - "To manage {{workspace}} workspace and view patient invites, go to": "To manage {{workspace}} workspace and view patient invites, go to", - "Tidepool Web": "Tidepool Web", - "Private Workspace": "Private Workspace", - "your private": "your private", - "You have been signed out of your session.": "You have been signed out of your session.", - "For security reasons, we automatically sign you out after a certain period of inactivity, or if you've signed out from another browser tab.": "For security reasons, we automatically sign you out after a certain period of inactivity, or if you've signed out from another browser tab.", - "Please sign in again to continue.": "Please sign in again to continue.", - "Return to Login": "Return to Login", - "Plug in meter with EZSync cable": "Plug in meter with EZSync cable", - "Plug in meter with": "Plug in meter with", - "EZSync002B cable": "EZSync002B cable", - "Plug cable into meter and then connect to computer": "Plug cable into meter and then connect to computer", - "Please unplug cable from computer and try again": "Please unplug cable from computer and try again", - "Plug cable into meter and then connect cable to computer": "Plug cable into meter and then connect cable to computer", "True Metrix, True Metrix Pro & True Metrix Air: Place meter in cradle • True Metrix Go: Plug in meter with micro-USB cable": "True Metrix, True Metrix Pro & True Metrix Air: Place meter in cradle • True Metrix Go: Plug in meter with micro-USB cable", - "Dexcom G7 Receiver is not yet supported.": "Dexcom G7 Receiver is not yet supported.", "mg/dL": "mg/dL", "mmol/L": "mmol/L", "Clinic Manager": "Clinic Manager", @@ -91,5 +59,10 @@ "Allowed special characters: - _ + > <": "Allowed special characters: - _ + > <", "Please select a duration period": "Please select a duration period", "Please select a last upload date option": "Please select a last upload date option", - "Please select at least one tag": "Please select at least one tag" + "Please select at least one tag": "Please select at least one tag", + "Newest version: {{newVersion}} - released {{newDate}}": "Newest version: {{newVersion}} - released {{newDate}}", + "Your version: {{currentVersion}} - installed {{currentDate}}": "Your version: {{currentVersion}} - installed {{currentDate}}", + "See what's new with Tidepool Uploader": "See what's new with Tidepool Uploader", + "You can continue to use your current version,": "You can continue to use your current version,", + "but we recommend updating as soon as possible.": "but we recommend updating as soon as possible." } \ No newline at end of file diff --git a/package.json b/package.json index 9007a978bc..2865ae1b22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tidepool-uploader", - "version": "2.58.0-remove-lzo-decompress.1", + "version": "2.58.0-react-oidc-context.4", "description": "Tidepool Project Universal Uploader", "private": true, "main": "main.prod.js", @@ -26,11 +26,14 @@ "build-renderer-quiet": "cross-env NODE_ENV=production webpack --config webpack.config.renderer.prod.babel.js", "build": "concurrently \"yarn build-main\" \"yarn build-renderer\"", "build-quiet": "concurrently \"yarn build-main-quiet\" \"yarn build-renderer-quiet\"", + "build-web": "cross-env NODE_ENV=production webpack --config webpack.config.web.dev.babel.js", "start": "cross-env NODE_ENV=production electron ./app/main.prod.js", "postinstall": "concurrently \"electron-builder install-app-deps\" \"node node_modules/fbjs-scripts/node/check-dev-engines.js package.json\"", "dev": "cross-env START_HOT=1 yarn start-renderer-dev", + "dev-web": "cross-env HOT=1 NODE_ENV=development webpack-dev-server --config webpack.config.web.dev.babel.js", "start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.renderer.dev.babel.js", - "start-main-dev": "cross-env HOT=1 NODE_ENV=development electron -r @babel/register ./app/main.dev.js", + "start-main-dev": "cross-env HOT=1 NODE_ENV=development electron --no-sandbox -r @babel/register ./app/main.dev.js", + "server": "node server", "prepare-qa-build": "node -r @babel/register scripts/prepare-qa-build.js", "package": "npm run build-quiet && electron-builder -p always -c electron-builder-publish.js" }, @@ -45,25 +48,30 @@ "async": "2.6.3", "babyparse": "0.4.6", "ble-glucose": "0.6.0", + "body-parser": "1.18.3", "bows": "1.7.2", "chrome-launcher": "0.15.2", "classnames": "2.5.1", "commander": "4.1.1", "connected-react-router": "6.9.3", "core-js": "2.6.12", - "cp2102": "0.0.7", + "cp2102": "0.1.1", + "cross-env": "7.0.3", "electron-debug": "3.2.0", "electron-is-dev": "2.0.0", "electron-log": "4.4.8", "electron-updater": "6.1.7", "es6-error": "4.1.1", + "express": "4.16.3", "fast-safe-stringify": "2.1.1", - "ftdi-js": "0.3.0", + "ftdi-js": "0.4.1", + "helmet": "7.0.0", "history": "4.10.1", "i18n-iso-countries": "6.1.0", "i18next": "20.6.1", "i18next-fs-backend": "1.1.4", "iconv-lite": "0.6.3", + "idb-keyval": "6.0.2", "identity-obj-proxy": "3.0.0", "immutability-helper": "3.1.1", "is-electron": "2.2.2", @@ -71,14 +79,17 @@ "keycloak-js": "22.0.5", "lodash": "4.17.21", "lzo-wasm": "0.0.4", + "oidc-client-ts": "3.0.1", "os-name": "4.0.1", "pako": "2.1.0", - "pl2303": "0.0.9", + "pl2303": "0.1.0", "plist": "3.1.0", "prop-types": "15.8.1", "react": "16.14.0", "react-dom": "16.14.0", "react-hot-loader": "4.13.1", + "react-i18next": "7.13.0", + "react-oidc-context": "3.1.0", "react-redux": "7.2.6", "react-router-dom": "5.2.0", "react-select": "1.2.1", @@ -130,6 +141,7 @@ "@babel/preset-react": "7.23.3", "@babel/register": "7.23.7", "@babel/runtime-corejs2": "7.23.9", + "@electron/notarize": "2.2.1", "@jest-runner/electron": "3.0.1", "@tidepool/direct-io": "3.0.2", "aws-sdk": "2.1544.0", @@ -144,6 +156,7 @@ "babel-plugin-transform-react-remove-prop-types": "0.4.24", "chai": "4.4.1", "concurrently": "8.2.2", + "copy-webpack-plugin": "4.5.2", "cross-env": "7.0.3", "css-loader": "5.2.7", "difflet": "1.0.1", @@ -151,7 +164,6 @@ "electron": "27.3.0", "electron-builder": "24.9.1", "electron-devtools-installer": "3.2.0", - "@electron/notarize": "2.2.1", "electron-rebuild": "3.2.9", "enzyme": "3.11.0", "eslint": "8.56.0", @@ -176,6 +188,7 @@ "moment": "2.29.4", "object-invariant-test-helper": "0.1.1", "optimize-css-assets-webpack-plugin": "5.0.4", + "optional": "0.1.4", "redux-mock-store": "1.5.4", "salinity": "0.0.8", "sinon": "17.0.1", diff --git a/server.js b/server.js new file mode 100644 index 0000000000..2b913036a8 --- /dev/null +++ b/server.js @@ -0,0 +1,163 @@ +/* eslint-disable quotes */ +const http = require('http'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const express = require('express'); +const helmet = require('helmet'); +const bodyParser = require('body-parser'); +const crypto = require('crypto'); + +const config = require('./config.server.js'); + +const buildDir = 'dist'; +const staticDir = path.join(__dirname, buildDir); + +express.static.mime.define({'application/wasm': ['wasm']}); + +const app = express(); + +const nonceMiddleware = (req, res, next) => { + // Cache static html file to avoid reading it from the filesystem on each request + if (!global.html) { + console.log('Caching static HTML'); + global.html = fs.readFileSync(`${staticDir}/index.html`, 'utf8'); + } + if (!global.ssoHtml) { + console.log('Caching static HTML'); + global.ssoHtml = fs.readFileSync(`${staticDir}/silent-check-sso.html`, 'utf8'); + } + + // Set a unique nonce for each request + res.locals.nonce = crypto.randomBytes(16).toString('base64'); + console.log('Setting nonce', res.locals.nonce); + res.locals.htmlWithNonces = global.html.replace(/<(script)/g, `<$1 nonce="${res.locals.nonce}"`); + res.locals.ssoHtmlWithNonces = global.ssoHtml.replace(/<(script)/g, `<$1 nonce="${res.locals.nonce}"`); + next(); +}; + +app.use(helmet({crossOriginEmbedderPolicy: true})); + +app.use(nonceMiddleware, helmet.contentSecurityPolicy({ + directives: { + defaultSrc: ["'none'"], + baseUri: ["'none'"], + scriptSrc: [ + "'self'", + "'strict-dynamic'", + (req, res) => { + return `'nonce-${res.locals.nonce}'`; + }, + 'https://static.zdassets.com', + 'https://ekr.zdassets.com', + 'https://tidepoolsupport.zendesk.com', + 'wss://tidepoolsupport.zendesk.com', + 'wss://*.zopim.com', + (req) => { + return req.hostname !== 'app.tidepool.org' && "'unsafe-eval'"; //required for Pendo.io Designer + }, + (req) => { + return req.hostname !== 'app.tidepool.org' && "'unsafe-inline'"; //required for Pendo.io Designer + }, + 'https://app.pendo.io', + 'https://pendo-io-static.storage.googleapis.com', + 'https://cdn.pendo.io', + 'https://pendo-static-5707274877534208.storage.googleapis.com', + 'https://data.pendo.io', + ], + styleSrc: [ + "'self'", + 'blob:', + "'unsafe-inline'", + 'https://app.pendo.io', + 'https://cdn.pendo.io', + 'https://pendo-static-5707274877534208.storage.googleapis.com', + ], + imgSrc: [ + "'self'", + 'data:', + 'https://v2assets.zopim.io', + 'https://static.zdassets.com', + 'https://tidepoolsupport.zendesk.com', + 'https://support.tidepool.org', + 'https://cdn.pendo.io', + 'https://app.pendo.io', + 'https://pendo-static-5707274877534208.storage.googleapis.com', + 'https://data.pendo.io' + ], + fontSrc: ["'self'", 'data:'], + reportUri: '/event/csp-report/violation', + objectSrc: ['blob:'], + workerSrc: ["'self'", 'blob:'], + childSrc: ["'self'", 'blob:', 'https://docs.google.com', 'https://app.pendo.io'], + frameSrc: ['https://docs.google.com', 'https://app.pendo.io', '*.tidepool.org', 'localhost:*', 'tidepooluploader://*'], + connectSrc: [].concat([ + process.env.API_URL || 'localhost:*', + process.env.UPLOAD_URL || 'localhost:*', + process.env.DATA_URL || 'localhost:*', + process.env.BLIP_URL || 'localhost:*', + process.env.REALM_HOST, + 'https://api.github.com/repos/tidepool-org/uploader/releases', + 'https://static.zdassets.com', + 'https://ekr.zdassets.com', + 'https://tidepoolsupport.zendesk.com', + 'wss://tidepoolsupport.zendesk.com', + 'https://api.rollbar.com', + 'wss://*.zopim.com', + '*.tidepool.org', + '*.development.tidepool.org', + '*.integration.tidepool.org', + 'http://*.integration-test.tidepool.org', + 'https://app.pendo.io', + 'https://data.pendo.io', + 'https://pendo-static-5707274877534208.storage.googleapis.com', + ]).filter(src => src !== undefined), + frameAncestors: ['https://app.pendo.io', '*.tidepool.org', 'localhost:*'] + }, + reportOnly: false, +})); + +app.use(bodyParser.json({ + type: ['json', 'application/csp-report'], +})); + +app.get('/silent-check-sso.html', (req, res) => { + res.send(res.locals.ssoHtmlWithNonces); +}); + +app.use('/uploader', express.static(staticDir, { index: false })); + +//So that we can use react-router and browser history +app.get('*', (req, res) => { + res.send(res.locals.htmlWithNonces); +}); + +app.post('/event/csp-report/violation', (req, res) => { + if (req.body) { + console.log('CSP Violation: ', req.body); + } else { + console.log('CSP Violation: No data received!'); + } + res.status(204).end(); +}); + +// If no ports specified, just start on default HTTP port +if (!(config.httpPort || config.httpsPort)) { + config.httpPort = 3001; +} + +if (config.httpPort) { + app.server = http.createServer(app).listen(config.httpPort, () => { + console.log('Connect server started on port', config.httpPort); + console.log('Serving static directory "' + staticDir + '/"'); + }); +} + +if (config.httpsPort) { + https.createServer(config.httpsConfig, app).listen(config.httpsPort, () => { + console.log('Connect server started on HTTPS port', config.httpsPort); + console.log('Serving static directory "' + staticDir + '/"'); + }); +} + +module.exports = app; diff --git a/styles/components/Footer.module.less b/styles/components/Footer.module.less index b4c96f961d..477c80d10b 100644 --- a/styles/components/Footer.module.less +++ b/styles/components/Footer.module.less @@ -13,6 +13,8 @@ * not, you can obtain one from Tidepool Project at tidepool.org. */ +@import '../core/variables.less'; + .footer { composes: flexColumn from '../core/layout.module.less'; flex-shrink: 0; @@ -59,3 +61,9 @@ composes: lightGray from '../core/typography.module.less'; margin: 10px auto; } + +.betaWarning { + composes: large from '../core/typography.module.less'; + color: @red-error; + margin: 0px auto; +} diff --git a/styles/components/UpdateDriverModal.module.less b/styles/components/UpdateDriverModal.module.less deleted file mode 100644 index 0040a3cb60..0000000000 --- a/styles/components/UpdateDriverModal.module.less +++ /dev/null @@ -1,54 +0,0 @@ -@import '../core/variables.less'; - -.purpleCentered { - color: @purple-dark; - text-align: center; -} - -.modalWrap { - composes: flexRow from '../core/layout.module.less'; - justify-content: center; - position: absolute; - top: 0; left: 0; - background-color: rgba(0, 0, 0, 0.25); - width: 100%; - height: 100%; -} - -.modal { - composes: flexColumn from '../core/layout.module.less'; - min-width: 330px; - background-color: @gray-background; - border-radius: 5px; - border: solid 1px @gray-light; - padding: 5px; - justify-content: center; - align-self: center; - align-items: center; -} - -.title { - composes: small bold from '../core/typography.module.less'; - padding-top: 10px; - padding-bottom: 5px; -} - -.text { - composes: small from '../core/typography.module.less'; - text-align: center; - max-width: 260px; -} - -.actions { - padding: 15px; -} - -.button { - composes: btn btnPrimary from '../core/buttons.module.less'; - margin: 6px; -} - -.buttonSecondary { - composes: btn btnSecondary from '../core/buttons.module.less'; - margin: 6px; -} diff --git a/test/app/actions/async.test.js b/test/app/actions/async.test.js index dd0790f856..b2a8b9f55b 100644 --- a/test/app/actions/async.test.js +++ b/test/app/actions/async.test.js @@ -2169,11 +2169,6 @@ describe('Asynchronous Actions', () => { err.version = version; err.debug = `Code: ${err.code} | Version: ${version}`; const expectedActions = [ - { - type: actionTypes.CHOOSING_FILE, - payload: { userId, deviceKey }, - meta: {source: actionSources[actionTypes.CHOOSING_FILE]} - }, { type: actionTypes.READ_FILE_ABORTED, error: true, @@ -2187,10 +2182,10 @@ describe('Asynchronous Actions', () => { const store = mockStore(state); store.dispatch(async.readFile(userId, deviceKey, {name: 'data.csv'}, ext)); const actions = store.getActions(); - expect(actions[1].payload).to.deep.include({ + expect(actions[0].payload).to.deep.include({ message: ErrorMessages.E_FILE_EXT + ext }); - expectedActions[1].payload = actions[1].payload; + expectedActions[0].payload = actions[0].payload; expect(actions).to.deep.equal(expectedActions); }); }); diff --git a/test/app/actions/sync.test.js b/test/app/actions/sync.test.js index 0529acefc5..4df4439212 100644 --- a/test/app/actions/sync.test.js +++ b/test/app/actions/sync.test.js @@ -1285,103 +1285,6 @@ describe('Synchronous Actions', () => { }); }); - describe('checkingForDriverUpdate', () => { - test('should be an FSA', () => { - let action = sync.checkingForDriverUpdate(); - expect(isFSA(action)).to.be.true; - }); - - test('should create an action to indicate a driver update check', () => { - const expectedAction = { - type: actionTypes.CHECKING_FOR_DRIVER_UPDATE, - meta: {source: actionSources[actionTypes.CHECKING_FOR_DRIVER_UPDATE]} - }; - expect(sync.checkingForDriverUpdate()).to.deep.equal(expectedAction); - }); - }); - - describe('driverUpdateAvailable', () => { - const current = '1'; - const available = '2'; - test('should be an FSA', () => { - let action = sync.driverUpdateAvailable(current, available); - expect(isFSA(action)).to.be.true; - }); - - test('should create an action to indicate a driver update being available', () => { - const expectedAction = { - type: actionTypes.DRIVER_UPDATE_AVAILABLE, - payload: { current, available }, - meta: {source: actionSources[actionTypes.DRIVER_UPDATE_AVAILABLE]} - }; - expect(sync.driverUpdateAvailable(current, available)).to.deep.equal(expectedAction); - }); - }); - - describe('driverUpdateNotAvailable', () => { - const updateInfo = {'url':'http://example.com'}; - test('should be an FSA', () => { - let action = sync.driverUpdateNotAvailable(updateInfo); - expect(isFSA(action)).to.be.true; - }); - - test('should create an action to indicate no driver update available', () => { - const expectedAction = { - type: actionTypes.DRIVER_UPDATE_NOT_AVAILABLE, - meta: {source: actionSources[actionTypes.DRIVER_UPDATE_NOT_AVAILABLE]} - }; - expect(sync.driverUpdateNotAvailable(updateInfo)).to.deep.equal(expectedAction); - }); - }); - - describe('dismissDriverUpdateAvailable', () => { - test('should be an FSA', () => { - let action = sync.dismissDriverUpdateAvailable(); - expect(isFSA(action)).to.be.true; - }); - - test('should create an action to indicate user dismissing driver update available modal', () => { - const expectedAction = { - type: actionTypes.DISMISS_DRIVER_UPDATE_AVAILABLE, - meta: {source: actionSources[actionTypes.DISMISS_DRIVER_UPDATE_AVAILABLE]} - }; - expect(sync.dismissDriverUpdateAvailable()).to.deep.equal(expectedAction); - }); - }); - - describe('driverInstall', () => { - const updateInfo = {'url':'http://example.com'}; - test('should be an FSA', () => { - let action = sync.driverInstall(updateInfo); - expect(isFSA(action)).to.be.true; - }); - - test('should create an action to indicate a driver update install', () => { - const expectedAction = { - type: actionTypes.DRIVER_INSTALL, - meta: {source: actionSources[actionTypes.DRIVER_INSTALL]} - }; - expect(sync.driverInstall(updateInfo)).to.deep.equal(expectedAction); - }); - }); - - describe('driverUpdateShellOpts', () => { - const opts = {'url':'http://example.com'}; - test('should be an FSA', () => { - let action = sync.driverUpdateShellOpts(opts); - expect(isFSA(action)).to.be.true; - }); - - test('should create an action to set update script opts', () => { - const expectedAction = { - type: actionTypes.DRIVER_INSTALL_SHELL_OPTS, - payload: { opts }, - meta: {source: actionSources[actionTypes.DRIVER_INSTALL_SHELL_OPTS]} - }; - expect(sync.driverUpdateShellOpts(opts)).to.deep.equal(expectedAction); - }); - }); - describe('deviceTimeIncorrect', () => { const callback = () => {}, cfg = { config: 'value'}, diff --git a/test/app/reducers/misc.test.js b/test/app/reducers/misc.test.js index fa76c9baee..246f78ccf7 100644 --- a/test/app/reducers/misc.test.js +++ b/test/app/reducers/misc.test.js @@ -340,76 +340,6 @@ describe('misc reducers', () => { }); }); - describe('driverUpdateAvailable', () => { - test('should return the initial state', () => { - expect(misc.driverUpdateAvailable(undefined, {})).to.be.null; - }); - - test('should handle DRIVER_UPDATE_AVAILABLE', () => { - const payload = {'example':'info'}; - expect(misc.driverUpdateAvailable(undefined, { - type: actionTypes.DRIVER_UPDATE_AVAILABLE, - payload - })).to.deep.equal(payload); - }); - - test('should handle DRIVER_UPDATE_NOT_AVAILABLE', () => { - expect(misc.driverUpdateAvailable(undefined, { - type: actionTypes.DRIVER_UPDATE_NOT_AVAILABLE - })).to.be.false; - }); - - test('should handle DRIVER_INSTALL', () => { - expect(misc.driverUpdateAvailable(undefined, { - type: actionTypes.DRIVER_INSTALL - })).to.be.false; - }); - }); - - describe('driverUpdateAvailableDismissed', () => { - test('should return the initial state', () => { - expect(misc.driverUpdateAvailableDismissed(undefined, {})).to.be.null; - }); - - test('should handle CHECKING_FOR_DRIVER_UPDATE', () => { - expect(misc.driverUpdateAvailableDismissed(undefined, { - type: actionTypes.CHECKING_FOR_DRIVER_UPDATE - })).to.be.false; - }); - - test('should handle DISMISS_DRIVER_UPDATE_AVAILABLE', () => { - expect(misc.driverUpdateAvailableDismissed(undefined, { - type: actionTypes.DISMISS_DRIVER_UPDATE_AVAILABLE - })).to.be.true; - }); - }); - - describe('driverUpdateShellOpts', () => { - test('should return the initial state', () => { - expect(misc.driverUpdateShellOpts(undefined, {})).to.be.null; - }); - - test('should handle DRIVER_INSTALL_SHELL_OPTS', () => { - const payload = {'example':'info'}; - expect(misc.driverUpdateShellOpts(undefined, { - type: actionTypes.DRIVER_INSTALL_SHELL_OPTS, - payload - })).to.deep.equal(payload); - }); - }); - - describe('driverUpdateComplete', () => { - test('should return the initial state', () => { - expect(misc.driverUpdateComplete(undefined, {})).to.be.null; - }); - - test('should handle DRIVER_INSTALL', () => { - expect(misc.driverUpdateComplete(undefined, { - type: actionTypes.DRIVER_INSTALL - })).to.be.true; - }); - }); - describe('showingDeviceTimePrompt', () => { test('should return the initial state', () => { expect(misc.showingDeviceTimePrompt(undefined, {})).to.be.null; diff --git a/test/app/reducers/working.test.js b/test/app/reducers/working.test.js index 5327752321..5e7b66d08d 100644 --- a/test/app/reducers/working.test.js +++ b/test/app/reducers/working.test.js @@ -1181,100 +1181,6 @@ describe('working', () => { }); }); - describe('checkingDriverUpdate', () => { - describe('checkingForDriverUpdate', () => { - it('should leave checkingDriverUpdate.completed unchanged', () => { - expect(initialState.checkingDriverUpdate.completed).to.be.null; - - let requestAction = actions.sync.checkingForDriverUpdate(); - let requestState = reducer(initialState, requestAction); - - expect(requestState.checkingDriverUpdate.completed).to.be.null; - - let successAction = actions.sync.driverUpdateAvailable('1','2'); - let successState = reducer(requestState, successAction); - - expect(successState.checkingDriverUpdate.completed).to.be.true; - - let state = reducer(successState, requestAction); - expect(state.checkingDriverUpdate.completed).to.be.true; - expect(mutationTracker.hasMutated(tracked)).to.be.false; - }); - - it('should set checkingDriverUpdate.inProgress to be true', () => { - let initialStateForTest = _.merge({}, initialState); - let tracked = mutationTracker.trackObj(initialStateForTest); - let action = actions.sync.checkingForDriverUpdate(); - - expect(initialStateForTest.checkingDriverUpdate.inProgress).to.be.false; - - let state = reducer(initialStateForTest, action); - expect(state.checkingDriverUpdate.inProgress).to.be.true; - expect(mutationTracker.hasMutated(tracked)).to.be.false; - }); - }); - - describe('driverUpdateAvailable', () => { - it('should set checkingDriverUpdate.completed to be true', () => { - expect(initialState.checkingDriverUpdate.completed).to.be.null; - - let successAction = actions.sync.driverUpdateAvailable('1','2'); - let state = reducer(initialState, successAction); - - expect(state.checkingDriverUpdate.completed).to.be.true; - expect(mutationTracker.hasMutated(tracked)).to.be.false; - }); - - it('should set checkingDriverUpdate.inProgress to be false', () => { - - let initialStateForTest = _.merge({}, initialState, { - checkingDriverUpdate: { inProgress: true, notification: null }, - }); - - let tracked = mutationTracker.trackObj(initialStateForTest); - - let action = actions.sync.driverUpdateAvailable('1','2'); - - expect(initialStateForTest.checkingDriverUpdate.inProgress).to.be.true; - - let state = reducer(initialStateForTest, action); - - expect(state.checkingDriverUpdate.inProgress).to.be.false; - expect(mutationTracker.hasMutated(tracked)).to.be.false; - }); - }); - - describe('driverUpdateNotAvailable', () => { - it('should set checkingDriverUpdate.completed to be true', () => { - expect(initialState.checkingDriverUpdate.completed).to.be.null; - - let successAction = actions.sync.driverUpdateNotAvailable(); - let state = reducer(initialState, successAction); - - expect(state.checkingDriverUpdate.completed).to.be.true; - expect(mutationTracker.hasMutated(tracked)).to.be.false; - }); - - it('should set checkingDriverUpdate.inProgress to be false', () => { - - let initialStateForTest = _.merge({}, initialState, { - checkingDriverUpdate: { inProgress: true, notification: null }, - }); - - let tracked = mutationTracker.trackObj(initialStateForTest); - - let action = actions.sync.driverUpdateNotAvailable(); - - expect(initialStateForTest.checkingDriverUpdate.inProgress).to.be.true; - - let state = reducer(initialStateForTest, action); - - expect(state.checkingDriverUpdate.inProgress).to.be.false; - expect(mutationTracker.hasMutated(tracked)).to.be.false; - }); - }); - }); - describe('updateClinicPatient', () => { describe('request', () => { it('should leave updatingClinicPatient.completed unchanged', () => { diff --git a/version.sh b/version.sh new file mode 100755 index 0000000000..a575260af1 --- /dev/null +++ b/version.sh @@ -0,0 +1,8 @@ +# Read the node version from the .nvmrc file +NVMRC_FILE=".nvmrc" +if [[ -f "$NVMRC_FILE" ]]; then + ARTIFACT_NODE_VERSION=$(cat "$NVMRC_FILE") + export ARTIFACT_NODE_VERSION +fi + +export START_NODE_VERSION="${ARTIFACT_NODE_VERSION}" diff --git a/webpack.config.renderer.dev.babel.js b/webpack.config.renderer.dev.babel.js old mode 100755 new mode 100644 diff --git a/webpack.config.web.dev.babel.js b/webpack.config.web.dev.babel.js new file mode 100755 index 0000000000..8fe0841049 --- /dev/null +++ b/webpack.config.web.dev.babel.js @@ -0,0 +1,428 @@ +/* eslint-disable max-len */ +/** + * Build config for development process that uses Hot-Module-Replacement + * https://webpack.github.io/docs/hot-module-replacement-with-webpack.html + */ + +import webpack from 'webpack'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'; +import merge from 'webpack-merge'; +import baseConfig from './webpack.config.base'; +import cp from 'child_process'; +import { spawn } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import terser from 'terser'; +import optional from 'optional'; +import _ from 'lodash'; +import RollbarSourceMapPlugin from 'rollbar-sourcemap-webpack-plugin'; + +const isDev = process.env.NODE_ENV === 'development'; +const isTest = process.env.NODE_ENV === 'test'; +const isProd = process.env.NODE_ENV === 'production'; + +const VERSION_SHA = + process.env.CIRCLE_SHA1 || + process.env.APPVEYOR_REPO_COMMIT || + cp.execSync('git rev-parse HEAD', { cwd: __dirname, encoding: 'utf8' }); + +const {ROLLBAR_POST_TOKEN} = process.env; + +const port = process.env.PORT || 3005; +const devPublicPath = + process.env.WEBPACK_PUBLIC_PATH || `http://localhost:${port}`; + +if (process.env.DEBUG_ERROR === 'true') { + console.log('~ ~ ~ ~ ~ ~ ~ ~ ~ ~'); + console.log('### DEBUG MODE ###'); + console.log('~ ~ ~ ~ ~ ~ ~ ~ ~ ~'); + console.log(); +} + +const apiUrl = _.get( + optional('./config/local'), + 'environment.API_URL', + process.env.API_URL || null +); +const uploadUrl = _.get( + optional('./config/local'), + 'environment.UPLOAD_URL', + process.env.UPLOAD_URL || null +); +const dataUrl = _.get( + optional('./config/local'), + 'environment.DATA_URL', + process.env.DATA_URL || null +); +const blipUrl = _.get( + optional('./config/local'), + 'environment.BLIP_URL', + process.env.BLIP_URL || null +); +const i18nEnabled = _.get( + optional('./config/local'), + 'I18N_ENABLED', + process.env.I18N_ENABLED || null +); + +console.log('API_URL =', apiUrl); +console.log('UPLOAD_URL =', uploadUrl); +console.log('DATA_URL =', dataUrl); +console.log('BLIP_URL =', blipUrl); +console.log('I18N_ENABLED =', i18nEnabled); + +const output = { + path: path.join(__dirname, 'dist'), + publicPath: isDev ? `${devPublicPath}/` : '/uploader/', + filename: 'bundle.js', + libraryTarget: 'umd', +}; + +const entry = isDev + ? [ + `webpack-dev-server/client?http://localhost:${port}/`, + 'webpack/hot/only-dev-server', + require.resolve('./app/index'), + ] + : [require.resolve('./app/index')]; + +let devtool = process.env.WEBPACK_DEVTOOL || 'inline-source-map'; +if (process.env.WEBPACK_DEVTOOL === false) devtool = undefined; + +let plugins = [ + new webpack.NoEmitOnErrorsPlugin(), + + /** + * Create global constants which can be configured at compile time. + * + * Useful for allowing different behaviour between development builds and + * release builds + * + * NODE_ENV should be production so that modules do not perform certain + * development checks + */ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': + JSON.stringify(process.env.NODE_ENV) || '"development"', + 'process.env.BUILD': JSON.stringify(process.env.BUILD) || '"dev"', + __DEBUG__: JSON.stringify(JSON.parse(process.env.DEBUG_ERROR || 'false')), + __VERSION_SHA__: JSON.stringify(VERSION_SHA), + 'global.GENTLY': false, // http://github.com/visionmedia/superagent/wiki/SuperAgent-for-Webpack for platform-client + 'process.env.API_URL': JSON.stringify(apiUrl), + 'process.env.UPLOAD_URL': JSON.stringify(uploadUrl), + 'process.env.DATA_URL': JSON.stringify(dataUrl), + 'process.env.BLIP_URL': JSON.stringify(blipUrl), + 'process.env.I18N_ENABLED': JSON.stringify(i18nEnabled), + }), + + new webpack.LoaderOptionsPlugin({ + debug: true, + }), + + new MiniCssExtractPlugin({ + filename: isDev ? 'style.css' : 'style.[contenthash].css', + }), + + new CopyWebpackPlugin([ + { + from: 'app/static', + transform: (content, path) => { + if (isDev || !path.endsWith('js')) { + return content; + } + + const code = fs.readFileSync(path, 'utf8'); + const result = terser.minify(code); + return result.code; + }, + }, + ]), + + new HtmlWebpackPlugin({ + template: 'app/web.ejs', + //inject: false, + }), + + new webpack.NamedModulesPlugin(), + + /** Upload sourcemap to Rollbar */ + ...(ROLLBAR_POST_TOKEN ? [new RollbarSourceMapPlugin({ + accessToken: ROLLBAR_POST_TOKEN, + version: VERSION_SHA, + publicPath: 'http://dynamichost/dist' + })] : []), +]; +let styleLoader = 'style-loader'; +if (isDev) { + plugins.push(new webpack.HotModuleReplacementPlugin()); +} else if (isProd) { + styleLoader = MiniCssExtractPlugin.loader; +} + +export default merge.smart(baseConfig, { + devtool, + + mode: isDev ? 'development' : 'production', + + target: 'web', + + entry, + + output, + + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + cacheDirectory: true, + }, + }, + }, + // https://github.com/ashtuchkin/iconv-lite/issues/204#issuecomment-432048618 + { + test: /node_modules[\/\\](iconv-lite)[\/\\].+/, + resolve: { + aliasFields: ['main'], + }, + }, + { + test: /\.global\.css$/, + use: [ + { + loader: styleLoader, + }, + { + loader: 'css-loader', + options: { + sourceMap: true, + }, + }, + ], + }, + + { + test: /^((?!\.global).)*\.css$/, + use: [ + { + loader: styleLoader, + }, + { + loader: 'css-loader', + options: { + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + sourceMap: true, + importLoaders: 1, + }, + }, + ], + }, + + { + test: /\.module\.less$/, + use: [ + { + loader: styleLoader, + }, + { + loader: 'css-loader', + options: { + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + sourceMap: true, + importLoaders: 1, + }, + }, + { + loader: 'less-loader', + options: { + sourceMap: true, + }, + }, + ], + }, + + { + test: /^((?!module).)*\.less$/, + use: [ + { + loader: styleLoader, + }, + { + loader: 'css-loader', + + options: { + sourceMap: true, + }, + }, + { + loader: 'less-loader', + + options: { + sourceMap: true, + }, + }, + ], + }, + + { + test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'url-loader', + + options: { + limit: 10000, + mimetype: 'application/font-woff', + }, + }, + ], + }, + { + test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'url-loader', + + options: { + limit: 10000, + mimetype: 'application/font-woff', + }, + }, + ], + }, + { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'url-loader', + + options: { + limit: 10000, + mimetype: 'application/octet-stream', + }, + }, + ], + }, + { + test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'file-loader', + }, + ], + }, + { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'url-loader', + + options: { + limit: 10000, + mimetype: 'image/svg+xml', + }, + }, + ], + }, + + { + test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10000, + }, + }, + ], + }, + { + test: /\.wasm$/, + type: 'javascript/auto', + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + }, + }, + ], + }, + ], + }, + resolve: { + alias: { + 'react-dom': '@hot-loader/react-dom', + }, + }, + optimization: { + minimizer: isDev + ? [] + : [ + new TerserPlugin({ + parallel: true, + sourceMap: true, + cache: true, + terserOptions: { + mangle: false + }, + extractComments: false + }), + new OptimizeCSSAssetsPlugin({ + cssProcessorOptions: { + map: { + inline: false, + annotation: true + } + } + }) + ] + }, + plugins, + + node: { + __dirname: true, // https://github.com/visionmedia/superagent/wiki/SuperAgent-for-Webpack for platform-client + __filename: false, + fs: 'empty', + dns: 'empty', + child_process: 'empty', + }, + + devServer: { + clientLogLevel: 'debug', + port, + publicPath: devPublicPath, + compress: true, + noInfo: true, + stats: 'errors-only', + inline: true, + lazy: false, + hot: true, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }, + contentBase: path.join(__dirname), + watchOptions: { + aggregateTimeout: 300, + ignored: /node_modules/, + poll: 100, + }, + historyApiFallback: { + verbose: true, + disableDotRule: false, + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 2e323f1f2c..0c233c1178 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2791,6 +2791,11 @@ resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz#5d8560d0d9f585ffc80865bc773db7bc975b680c" integrity sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw== +"@types/w3c-web-usb@^1.0.6": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz#cf89cccd2d93b6245e784c19afe0a9f5038d4528" + integrity sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ== + "@types/webpack-sources@*": version "3.2.3" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.3.tgz#b667bd13e9fa15a9c26603dce502c7985418c3d8" @@ -4020,7 +4025,7 @@ bluebird-lst@^1.0.9: dependencies: bluebird "^3.5.5" -bluebird@^3.5.5: +bluebird@^3.5.1, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -4035,6 +4040,38 @@ bn.js@^5.0.0, bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +body-parser@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + integrity sha512-XIXhPptoLGNcvFyyOzjNXCjDYIbYj4iuXO0VU9lM0f3kYdG0ar5yg7C+pIc3OyoTlZXDu5ObpLTmS2Cgp89oDg== + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + raw-body "2.3.2" + type-is "~1.6.15" + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ== + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -4302,6 +4339,25 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" + integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA== + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^2.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^5.2.4" + unique-filename "^1.1.0" + y18n "^4.0.0" + cacache@^12.0.2: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -4606,7 +4662,7 @@ chokidar@^3.4.1: optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.1: +chownr@^1.0.1, chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -4982,6 +5038,11 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -5009,6 +5070,11 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw== + cookie@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" @@ -5043,6 +5109,20 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== +copy-webpack-plugin@4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz#d53444a8fea2912d806e78937390ddd7e632ee5c" + integrity sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ== + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + globby "^7.1.1" + is-glob "^4.0.0" + loader-utils "^1.1.0" + minimatch "^3.0.4" + p-limit "^1.0.0" + serialize-javascript "^1.4.0" + core-js-compat@^3.31.0, core-js-compat@^3.34.0: version "3.35.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.1.tgz#215247d7edb9e830efa4218ff719beb2803555e2" @@ -5050,11 +5130,6 @@ core-js-compat@^3.31.0, core-js-compat@^3.34.0: dependencies: browserslist "^4.22.2" -core-js@2.6.10: - version "2.6.10" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" - integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== - core-js@2.6.11: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" @@ -5101,12 +5176,13 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cp2102@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/cp2102/-/cp2102-0.0.7.tgz#ae1b57b5a022bf240e29585dfbd3d40282c1fc4a" - integrity sha512-ZCb9ynhYi5VlXdseJm9AV7cbvQZ2P83eT5bHc6nfStEi7ctXaZCj25fKCVNFUBu5040FXzCaMMz6hNGvWduBKg== +cp2102@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/cp2102/-/cp2102-0.1.1.tgz#803cd7f05fed2ddab75834528ed5c3f3581bdeb3" + integrity sha512-oE1nO/AOLzTeDIyhcZRo7Y+PG7SsYntkw200BLBpMIICAHC5KBcWw8og9NiOLzHd5ZwnGZ9wTk3E1zLyEkmsdA== dependencies: - usb "2.4.3" + core-js "2.6.12" + usb "2.11.0" crc@^3.8.0: version "3.8.0" @@ -5614,12 +5690,17 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + integrity sha512-Jlk9xvkTDGXwZiIDyoM7+3AsuvJVoyOpRupvEVy9nX3YO3/ieZxhlgh8GpLNZ8AY7HjO6y2YwpMSh1ejhu3uIw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@~1.1.2: +depd@~1.1.1, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== @@ -5642,6 +5723,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== + detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -5708,6 +5794,13 @@ dir-compare@^3.0.0: buffer-equal "^1.0.0" minimatch "^3.0.4" +dir-glob@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" + integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== + dependencies: + path-type "^3.0.0" + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -6820,6 +6913,42 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== +express@4.16.3: + version "4.16.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" + integrity sha512-CDaOBMB9knI6vx9SpIxEMOJ6VBbC2U/tYNILs0qv1YOZc15K9U2EcF06v10F0JX6IYcWnKYZJwIDJspEHLvUaQ== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.2" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.3" + qs "6.5.1" + range-parser "~1.2.0" + safe-buffer "5.1.1" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + express@^4.17.1: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -7052,6 +7181,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + finalhandler@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" @@ -7073,6 +7215,15 @@ find-babel-config@^2.0.0: json5 "^2.1.1" path-exists "^4.0.0" +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + integrity sha512-46TFiBOzX7xq/PcSWfFwkyjpemdRnMe31UQF+os0y+1W3k95f6R4SEt02Hj4p3X0Mir9gfrkmOtshFidS0VPUg== + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -7096,6 +7247,13 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== + dependencies: + locate-path "^2.0.0" + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -7333,14 +7491,13 @@ fsevents@^2.1.2, fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -ftdi-js@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/ftdi-js/-/ftdi-js-0.3.0.tgz#25d878ba781b89a908f98532682c17ee8e582740" - integrity sha512-GTi5hFw9x4/xYEa5u3EAbj7B7uelkx8ThkduUsbgiDi7f7Tbzb50ss1J0DCwzFGGPT/P+N6bIKoLUCzKzXRhfw== +ftdi-js@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/ftdi-js/-/ftdi-js-0.4.1.tgz#ba0f7f970908bac9a2d39deec99fa44e7dbd63ec" + integrity sha512-+HB0YK+e5dEMFAilO2oeHWGIyQxVEDQm8I5A8FfN7W0/200z6NAikQH4vSqK/ELjtQGmwqcwDtsloEETjAuFiw== dependencies: - core-js "2.6.10" - is-electron "2.2.1" - usb "2.4.3" + core-js "2.6.12" + usb "2.11.0" ftp@0.3.10: version "0.3.10" @@ -7627,6 +7784,18 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + integrity sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g== + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -7805,6 +7974,11 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +helmet@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.0.0.tgz#ac3011ba82fa2467f58075afa58a49427ba6212d" + integrity sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ== + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" @@ -7831,6 +8005,11 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^2.3.1: + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== + hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -7927,6 +8106,13 @@ html-minifier-terser@^5.0.1: relateurl "^0.2.7" terser "^4.6.3" +html-parse-stringify2@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a" + integrity sha512-wMKQ3aJ/dwXzDHPpA7XgsRXXCkEhHkAF6Ioh7D51lgZO7Qy0LmcFddC9TI/qNQJvSM1KL8KbcR3FtuybsrzFlQ== + dependencies: + void-elements "^2.0.1" + html-webpack-plugin@4.5.2: version "4.5.2" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz#76fc83fa1a0f12dd5f7da0404a54e2699666bc12" @@ -7972,18 +8158,17 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== +http-errors@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + integrity sha512-STnYGcKMXL9CGdtpeTFnLmgMSHTTNQJSHxiC4DETHKf934Q160Ht5pljrNeH24S0O9xUN+9vsDJZdZtk5js6Ww== dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" -http-errors@~1.6.2: +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== @@ -7993,6 +8178,17 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" @@ -8104,6 +8300,18 @@ iconv-corefoundation@^1.1.7: cli-truncate "^2.1.0" node-addon-api "^1.6.3" +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -8123,6 +8331,13 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== +idb-keyval@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.0.2.tgz#5079bae35169594d571973ac5bb387d17f21ae60" + integrity sha512-He4/fj9peGfjms7KSm2VuI6jubGv57SiicBEkLXEn9WEAuR+3S2SvLlvOVrXHRooAodrP5d11WOXGjONMgIypQ== + dependencies: + safari-14-idb-fix "^3.0.0" + identity-obj-proxy@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" @@ -8145,6 +8360,11 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + ignore@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -8462,11 +8682,6 @@ is-docker@^2.0.0: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== -is-electron@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.1.tgz#751b1dd8a74907422faa5c35aaa0cf66d98086e9" - integrity sha512-r8EEQQsqT+Gn0aXFx7lTFygYQhILLCB+wn0WCDL5LZRINeLH/Rvw1j2oKodELLXYNImQ3CRlVsY8wW4cGOsyuw== - is-electron@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" @@ -9810,6 +10025,11 @@ just-extend@^6.2.0: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + keyboardevent-from-electron-accelerator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-2.0.0.tgz#ace21b1aa4e47148815d160057f9edb66567c50c" @@ -9984,6 +10204,14 @@ loader-utils@^2.0.0, loader-utils@^2.0.3: emojis-list "^3.0.0" json5 "^2.1.2" +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -10137,7 +10365,7 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^4.0.1: +lru-cache@^4.0.1, lru-cache@^4.1.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -10193,6 +10421,13 @@ macos-release@^2.5.0: resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.1.tgz#bccac4a8f7b93163a8d163b8ebf385b3c5f55bf9" integrity sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A== +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -10380,6 +10615,11 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, dependencies: mime-db "1.52.0" +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -10530,6 +10770,22 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -10751,6 +11007,11 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== +node-addon-api@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.0.tgz#71f609369379c08e251c558527a107107b5e0fdb" + integrity sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g== + node-api-version@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/node-api-version/-/node-api-version-0.1.4.tgz#1ed46a485e462d55d66b5aa1fe2821720dedf080" @@ -10777,7 +11038,7 @@ node-forge@^0.10.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== -node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: +node-gyp-build@^4.2.1, node-gyp-build@^4.3.0, node-gyp-build@^4.5.0: version "4.8.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== @@ -11078,6 +11339,13 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +oidc-client-ts@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7" + integrity sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg== + dependencies: + jwt-decode "^4.0.0" + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -11085,6 +11353,13 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + on-headers@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" @@ -11124,6 +11399,11 @@ optimize-css-assets-webpack-plugin@5.0.4: cssnano "^4.1.10" last-call-webpack-plugin "^3.0.0" +optional@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/optional/-/optional-0.1.4.tgz#cdb1a9bedc737d2025f690ceeb50e049444fd5b3" + integrity sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -11191,6 +11471,13 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== +p-limit@^1.0.0, p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -11205,6 +11492,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== + dependencies: + p-limit "^1.1.0" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -11245,6 +11539,11 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -11436,6 +11735,13 @@ path-to-regexp@^6.2.1: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -11487,6 +11793,11 @@ pify@^2.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -11509,6 +11820,13 @@ pirates@^4.0.1, pirates@^4.0.6: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha512-ojakdnUgL5pzJYWw2AIDEupaQCX5OPbM688ZevubICjdIX01PRSYKqm33fJoCOJBRseYCTUlQRnBNX+Pchaejw== + dependencies: + find-up "^2.1.0" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -11530,12 +11848,12 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -pl2303@0.0.9: - version "0.0.9" - resolved "https://registry.yarnpkg.com/pl2303/-/pl2303-0.0.9.tgz#48a6bc29bb2e0d221832413559477daacd888dc9" - integrity sha512-QK47mT84x4pv37RB3igjjVI4QmKNtfBKira24/u1j2GGKAIM78pK0xUEhcJdoG324LIYg6ohUek/NpIuWWj3yQ== +pl2303@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/pl2303/-/pl2303-0.1.0.tgz#7302d428d775c3db3a660e19343c5a7130487067" + integrity sha512-hsggk/eRyVqEa1MaD0J8uYfHLUqG0ITzya2NazxGm7fFI3LzQIJaC4+g7u+dYh3fzK2MOOEcgT6j70VIi/BASw== dependencies: - usb "2.4.3" + usb "2.11.0" plist@3.1.0, plist@^3.0.4, plist@^3.0.5: version "3.1.0" @@ -12008,7 +12326,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@15.8.1, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -12022,7 +12340,7 @@ property-expr@^2.0.4: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== -proxy-addr@~2.0.7: +proxy-addr@~2.0.3, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -12057,7 +12375,7 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" -pump@^2.0.0: +pump@^2.0.0, pump@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== @@ -12109,6 +12427,16 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== + +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + qs@^6.11.2, qs@^6.9.1: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" @@ -12189,11 +12517,31 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -range-parser@^1.2.1, range-parser@~1.2.1: +range-parser@^1.2.1, range-parser@~1.2.0, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + integrity sha512-Ss0DsBxqLxCmQkfG5yazYhtbVVTJqS9jTsZG2lhrNwqzOk2SUC7O/NB/M//CkEBqsrtmlNgJCPccJGuYSFr6Vg== + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + raw-body@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" @@ -12243,6 +12591,15 @@ react-hot-loader@4.13.1: shallowequal "^1.1.0" source-map "^0.7.3" +react-i18next@7.13.0: + version "7.13.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.13.0.tgz#a6f64fd749215ec70400f90da6cbde2a9c5b1588" + integrity sha512-35M+MZFPqHwVIas7tXWQKFrf+ozCJukNplUTiGqL8mczSk+VRBsHxxXuuQKRkz/4CcWkONGWbp/AzxfM6wZncg== + dependencies: + hoist-non-react-statics "^2.3.1" + html-parse-stringify2 "2.0.1" + prop-types "^15.6.0" + react-input-autosize@^2.1.2: version "2.2.2" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2" @@ -12270,6 +12627,11 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-oidc-context@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-oidc-context/-/react-oidc-context-3.1.0.tgz#1047ee859b12793132854d583eaf0160b8a05d4f" + integrity sha512-ceQztvDfdl28mbr0So31XF/tCJamyF1+nm4AQNIE/nub+Xs9PLtDqLy/+75Yx1ahI0/n3nsq0R2qcP0R2Laa3Q== + react-redux@7.2.6: version "7.2.6" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" @@ -12771,7 +13133,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg== -rimraf@^2.5.4, rimraf@^2.6.3: +rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -12872,6 +13234,11 @@ rxjs@^7.8.1: dependencies: tslib "^2.1.0" +safari-14-idb-fix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440" + integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog== + safe-array-concat@^1.0.0, safe-array-concat@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" @@ -12882,6 +13249,11 @@ safe-array-concat@^1.0.0, safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" +safe-buffer@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -13052,6 +13424,25 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -13078,6 +13469,11 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +serialize-javascript@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -13098,6 +13494,16 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + serve-static@1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" @@ -13148,6 +13554,11 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + integrity sha512-9jphSf3UbIgpOX/RKvX02iw/rN2TKdusnsPpGfO/rkcsrd+IRqgHZb4VGnmL0Cynps8Nj2hN45wsi30BzrHDIw== + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -13304,6 +13715,11 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg== + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -13537,6 +13953,13 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ== + dependencies: + safe-buffer "^5.1.1" + ssri@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" @@ -13605,11 +14028,16 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -14383,7 +14811,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.18: +type-is@~1.6.15, type-is@~1.6.16, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -14505,7 +14933,7 @@ uniqs@^2.0.0: resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" integrity sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ== -unique-filename@^1.1.1: +unique-filename@^1.1.0, unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== @@ -14641,14 +15069,14 @@ usb-cdc-acm@0.1.1: debug "^3.1.0" usb "^1.3.1" -usb@2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/usb/-/usb-2.4.3.tgz#fab8c1820276d0cb34f87a5ddba0152633d4a829" - integrity sha512-BmCjjxsriODcrb+TdXdzSDDys+MOUeRvo22ywmyIxYcuVW9YKrr2Wp2gQZUfHrQbvovKu87Po9mk5OPftJdDeQ== +usb@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/usb/-/usb-2.11.0.tgz#bbb2257c65534635a450aed3754df7c8844d518e" + integrity sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg== dependencies: - "@types/w3c-web-usb" "1.0.6" - node-addon-api "^4.2.0" - node-gyp-build "^4.3.0" + "@types/w3c-web-usb" "^1.0.6" + node-addon-api "^7.0.0" + node-gyp-build "^4.5.0" usb@2.5.2: version "2.5.2" @@ -14820,6 +15248,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +void-elements@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + w3c-hr-time@^1.0.1, w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"