Page MenuHomePhorge

D1447.diff
No OneTemporary

D1447.diff

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.env b/.env
new file mode 120000
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+src/.env
\ No newline at end of file
diff --git a/bin/phpstan b/bin/phpstan
--- a/bin/phpstan
+++ b/bin/phpstan
@@ -4,7 +4,7 @@
pushd ${cwd}/../src/
-php -dmemory_limit=320M \
+php -dmemory_limit=350M \
vendor/bin/phpstan \
analyse
diff --git a/bin/quickstart.sh b/bin/quickstart.sh
--- a/bin/quickstart.sh
+++ b/bin/quickstart.sh
@@ -33,15 +33,11 @@
base_dir=$(dirname $(dirname $0))
-bin/regen-certs
-
-docker pull kolab/centos7:latest
+docker pull docker.io/kolab/centos7:latest
docker-compose down
docker-compose build
-docker-compose up -d kolab mariadb redis
-
pushd ${base_dir}/src/
cp .env.example .env
@@ -51,6 +47,14 @@
cat .env.local >> .env
fi
+popd
+
+bin/regen-certs
+
+docker-compose up -d coturn kolab mariadb openvidu proxy redis
+
+pushd ${base_dir}/src/
+
rm -rf vendor/ composer.lock
php -dmemory_limit=-1 /bin/composer install
npm install
diff --git a/bin/regen-certs b/bin/regen-certs
--- a/bin/regen-certs
+++ b/bin/regen-certs
@@ -2,22 +2,22 @@
base_dir=$(dirname $(dirname $0))
-base_dir="${base_dir}/docker/certs/"
+cert_dir="${base_dir}/docker/certs/"
-if [ ! -d "${base_dir}" ]; then
- mkdir -p ${base_dir}
+if [ ! -d "${cert_dir}" ]; then
+ mkdir -p ${cert_dir}
fi
-if [ ! -f "${base_dir}/ca.key" ]; then
- openssl genrsa -out ${base_dir}/ca.key 4096
+if [ ! -f "${cert_dir}/ca.key" ]; then
+ openssl genrsa -out ${cert_dir}/ca.key 4096
openssl req \
-new \
-x509 \
-nodes \
-days 3650 \
- -key ${base_dir}/ca.key \
- -out ${base_dir}/ca.cert \
+ -key ${cert_dir}/ca.key \
+ -out ${cert_dir}/ca.cert \
-subj '/O=Example CA/'
fi
@@ -30,13 +30,15 @@
exit 1
fi
-for name in kolab.mgmt.com kolab.hosted.com; do
- openssl genrsa -out ${base_dir}/${name}.key 4096
+export $(cat ${base_dir}/src/.env | xargs) >/dev/null 2>&1
+
+for name in kolab.mgmt.com kolab.hosted.com {{admin,meet}.,}${APP_DOMAIN}; do
+ openssl genrsa -out ${cert_dir}/${name}.key 4096
openssl req \
-new \
- -key ${base_dir}/${name}.key \
- -out ${base_dir}/${name}.csr \
+ -key ${cert_dir}/${name}.key \
+ -out ${cert_dir}/${name}.csr \
-subj "/O=Example CA/CN=${name}/" \
-reqexts SAN \
-config <(cat ${openssl_cnf} \
@@ -44,11 +46,11 @@
openssl x509 \
-req \
- -in ${base_dir}/${name}.csr \
- -CA ${base_dir}/ca.cert \
- -CAkey ${base_dir}/ca.key \
+ -in ${cert_dir}/${name}.csr \
+ -CA ${cert_dir}/ca.cert \
+ -CAkey ${cert_dir}/ca.key \
-CAcreateserial \
- -out ${base_dir}/${name}.cert \
+ -out ${cert_dir}/${name}.cert \
-days 28 \
-extfile <(cat ${openssl_cnf} \
<(printf "[SAN]\nsubjectAltName=DNS:${name}")) \
@@ -58,8 +60,13 @@
openssl pkcs8 \
-topk8 \
-inform pem \
- -in ${base_dir}/${name}.key \
+ -in ${cert_dir}/${name}.key \
-outform pem \
-nocrypt \
- -out ${base_dir}/${name}_p8.key
+ -out ${cert_dir}/${name}_p8.key
+
+ cat ${cert_dir}/${name}.cert \
+ ${cert_dir}/ca.cert > ${cert_dir}/${name}.chain.pem
+
+ chmod 644 ${cert_dir}/*.{cert,key,pem}
done
diff --git a/docker-compose.yml b/docker-compose.yml
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,18 @@
version: '3'
services:
+ coturn:
+ container_name: kolab-coturn
+ environment:
+ - DB_NAME=${OPENVIDU_COTURN_REDIS_DATABASE}
+ - DB_PASSWORD=${OPENVIDU_COTURN_REDIS_PASSWORD}
+ - REDIS_IP=${OPENVIDU_COTURN_REDIS_IP}
+ - TURN_PUBLIC_IP=${OPENVIDU_COTURN_IP}
+ - TURN_LISTEN_PORT=3478
+ hostname: sturn.mgmt.com
+ image: openvidu/openvidu-coturn:1.0.0
+ network_mode: host
+ restart: on-failure
+ tty: true
kolab:
build:
context: ./docker/kolab/
@@ -23,6 +36,7 @@
- /var/tmp
tty: true
volumes:
+ - /etc/letsencrypt/:/etc/letsencrypt/:ro
- ./docker/certs/ca.cert:/etc/pki/tls/certs/ca.cert:ro
- ./docker/certs/ca.cert:/etc/pki/ca-trust/source/anchors/ca.cert:ro
- ./docker/certs/kolab.hosted.com.cert:/etc/pki/tls/certs/kolab.hosted.com.cert
@@ -30,6 +44,7 @@
- ./docker/certs/kolab.mgmt.com.cert:/etc/pki/tls/certs/kolab.mgmt.com.cert
- ./docker/certs/kolab.mgmt.com.key:/etc/pki/tls/certs/kolab.mgmt.com.key
- ./docker/kolab/utils:/root/utils:ro
+ - ./src/.env:/.dockerenv:ro
- /sys/fs/cgroup:/sys/fs/cgroup:ro
mariadb:
container_name: kolab-mariadb
@@ -42,6 +57,53 @@
retries: 30
image: mariadb
network_mode: host
+ openvidu:
+ container_name: kolab-openvidu
+ environment:
+ - APP_DOMAIN=${APP_DOMAIN}
+ - CERTIFICATE_TYPE=letsencrypt
+ - COTURN_IP=${OPENVIDU_COTURN_IP}
+ - COTURN_REDIS_DBNAME=${OPENVIDU_COTURN_REDIS_DATABASE}
+ - COTURN_REDIS_PASSWORD=${OPENVIDU_COTURN_REDIS_PASSWORD}
+ - COTURN_REDIS_IP=${OPENVIDU_COTURN_REDIS_IP}
+ - DOMAIN_OR_PUBLIC_IP=${OPENVIDU_PUBLIC_IP}
+ - HTTP_PORT=8080
+ - HTTPS_PORT=443
+ - KMS_STUN_IP=${OPENVIDU_COTURN_IP}
+ - KMS_STUN_PORT=3478
+ - KMS_URIS=["ws://localhost:8888/kurento"]
+ - OPENVIDU_SECRET=${OPENVIDU_API_PASSWORD}
+ - OPENVIDU_WEBHOOK=${OPENVIDU_WEBHOOK}
+ - OPENVIDU_WEBHOOK_ENDPOINT=${OPENVIDU_WEBHOOK_ENDPOINT}
+ - SERVER_PORT=8443
+ hostname: openvidu.hosted.com
+ image: openvidu/openvidu-server-kms:2.15.0
+ network_mode: host
+ tmpfs:
+ - /run
+ - /tmp
+ - /var/run
+ - /var/tmp
+ tty: true
+ volumes:
+ - /etc/letsencrypt/:/etc/letsencrypt/:ro
+ proxy:
+ build:
+ context: ./docker/proxy/
+ container_name: kolab-proxy
+ hostname: kanarip.internet-box.ch
+ image: kolab-proxy
+ network_mode: host
+ tmpfs:
+ - /run
+ - /tmp
+ - /var/run
+ - /var/tmp
+ tty: true
+ volumes:
+ - ./docker/certs/:/etc/certs/:ro
+ - /etc/letsencrypt/:/etc/letsencrypt/:ro
+ - /sys/fs/cgroup:/sys/fs/cgroup:ro
redis:
build:
context: ./docker/redis/
diff --git a/docker/coturn/Dockerfile b/docker/coturn/Dockerfile
new file mode 100644
--- /dev/null
+++ b/docker/coturn/Dockerfile
@@ -0,0 +1,37 @@
+FROM fedora:31
+
+MAINTAINER Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>
+
+RUN dnf -y install \
+ --setopt 'tsflags=nodocs' \
+ bash-completion \
+ bind-utils \
+ coturn \
+ curl \
+ dhcp-client \
+ iproute \
+ iptraf-ng \
+ iputils \
+ less \
+ lsof \
+ mtr \
+ net-tools \
+ NetworkManager \
+ NetworkManager-tui \
+ network-scripts \
+ nmap-ncat \
+ openssh-clients \
+ openssh-server \
+ procps-ng \
+ strace \
+ systemd-udev \
+ tcpdump \
+ telnet \
+ traceroute \
+ vim-enhanced \
+ wget && \
+ dnf clean all
+
+COPY rootfs/ /
+
+CMD ["/usr/local/bin/coturn.sh"]
diff --git a/docker/coturn/rootfs/usr/local/bin/coturn.sh b/docker/coturn/rootfs/usr/local/bin/coturn.sh
new file mode 100755
--- /dev/null
+++ b/docker/coturn/rootfs/usr/local/bin/coturn.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+cd /tmp/
+
+cat > ./turnserver.conf << EOF
+external-ip=${TURN_PUBLIC_IP:-127.0.0.1}
+listening-port=${TURN_LISTEN_PORT:-3478}
+fingerprint
+lt-cred-mech
+max-port=${MAX_PORT:-65535}
+min-port=${MIN_PORT:-40000}
+pidfile="$(pwd)/turnserver.pid"
+realm=openvidu
+simple-log
+redis-userdb="ip=${REDIS_IP:-127.0.0.1} dbname=${DB_NAME:-2} connect_timeout=30"
+verbose
+EOF
+
+/usr/bin/turnserver -c ./turnserver.conf
diff --git a/docker/kolab/Dockerfile b/docker/kolab/Dockerfile
--- a/docker/kolab/Dockerfile
+++ b/docker/kolab/Dockerfile
@@ -63,6 +63,9 @@
RUN sed -i -r -e 's/^SELINUX=.*$/SELINUX=permissive/g' /etc/selinux/config 2>/dev/null || :
+RUN sed -i -r -e 's/^Listen 80$/Listen 9080/g' /etc/httpd/conf/httpd.conf
+#RUN sed -i -r -e 's/^Listen 443$/Listen 9443/g' /etc/httpd/conf/httpd.conf
+
COPY kolab-init.sh /usr/local/sbin/
RUN chmod 750 /usr/local/sbin/kolab-init.sh
COPY kolab-vlv.sh /usr/local/sbin/
diff --git a/docker/kurento-media-server/Dockerfile b/docker/kurento-media-server/Dockerfile
new file mode 100644
--- /dev/null
+++ b/docker/kurento-media-server/Dockerfile
@@ -0,0 +1,219 @@
+FROM fedora:31
+
+MAINTAINER Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>
+
+# From https://github.com/Kurento/kurento-media-server/blob/master/debian/control
+#
+# build-essential,
+# cmake,
+# gstreamer1.5-plugins-good,
+# kms-cmake-utils (>= 6.7.0),
+# kms-core-dev (>= 6.7.0),
+# kms-elements-dev (>= 6.7.0),
+# kms-filters-dev (>= 6.7.0),
+# kurento-module-creator (>= 6.7.0),
+# libboost-dev,
+# libboost-filesystem-dev,
+# libboost-log-dev,
+# libboost-program-options-dev,
+# libboost-system-dev,
+# libboost-test-dev,
+# libboost-thread-dev,
+# libevent-dev,
+# libglibmm-2.4-dev,
+# libgstreamer-plugins-base1.5-dev,
+# libsigc++-2.0-dev,
+# libwebsocketpp-dev
+
+ENV CFLAGS="-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -fasynchronous-unwind-tables -fstack-clash-protection -flto"
+ENV CXXFLAGS=${CFLAGS}
+ENV LDFLAGS='-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -flto -fuse-linker-plugin'
+ENV CC=gcc
+ENV CXX=g++
+ENV AR=/bin/gcc-ar
+ENV RANLIB=/bin/gcc-ranlib
+ENV NM=/bin/gcc-nm
+ENV CMAKE_CXX_ARCHIVE_CREATE="<CMAKE_AR> qcs <TARGET> <LINK_FLAGS> <OBJECTS>"
+
+ARG CMAKEOPTS="-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/ -DCMAKE_INSTALL_SYSCONFDIR=/etc/ -DCMAKE_MODULES_INSTALL_DIR=/usr/share/cmake/Modules/ -DBUILD_SHARED_LIBS=ON -DCMAKE_AR=${AR} -DCMAKE_CXX_ARCHIVE_FINISH=true -DCMAKE_MODULES_INSTALL_DIR=/usr/share/cmake/Modules/ -DGENERATE_TESTS=FALSE -DDISABLE_TESTS=TRUE"
+ARG KURENTO_VERSION="6.14.0"
+ARG KURENTO_JSONCPP_VERSION="1.6.4"
+
+RUN dnf -y install \
+ boost-devel \
+ cmake \
+ coreutils \
+ #ffmpeg \
+ gcc-c++ \
+ git \
+ glibmm24-devel \
+ gstreamer1-devel \
+ gstreamer1-plugins-base-devel \
+ gstreamer1-plugins-bad-free-devel \
+ gstreamer1-plugins-good \
+ libevent-devel \
+ libnice-devel \
+ libsigc++20-devel \
+ libsoup-devel \
+ libuuid-devel \
+ libvpx-devel \
+ make \
+ maven \
+ opencv-devel \
+ openssl-devel \
+ redhat-rpm-config \
+ vim-enhanced \
+ websocketpp-devel && \
+ mkdir -p /src/ && \
+ git clone https://github.com/Kurento/kms-cmake-utils.git /src/kms-cmake-utils.git && \
+ cd /src/kms-cmake-utils.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ .. && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ git clone https://github.com/Kurento/kurento-module-creator.git /src/kurento-module-creator.git && \
+ cd /src/kurento-module-creator.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ mvn clean install -DskipTests && \
+ cp target/kurento-module-creator-jar-with-dependencies.jar /usr/bin/. && \
+ cp scripts/kurento-module-creator /usr/bin/. && \
+ cp target/classes/*.cmake /usr/share/cmake/Modules/. && \
+ git clone https://github.com/Kurento/jsoncpp.git /src/jsoncpp.git && \
+ cd /src/jsoncpp.git/ && \
+ git checkout ${KURENTO_JSONCPP_VERSION} && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ -DJSONCPP_WITH_CMAKE_PACKAGE=ON \
+ -DLIBRARY_INSTALL_DIR=/usr/lib64/ \
+ -DPACKAGE_INSTALL_DIR=/usr/share/cmake/ \
+ .. && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ mv -v /usr/lib/pkgconfig/*.pc /usr/share/pkgconfig/. && \
+ mv -v /usr/share/cmake/jsoncpp/*.cmake /usr/share/cmake/Modules/. && \
+ rm -rvf /usr/share/cmake/jsoncpp/ && \
+ mv -v /usr/include/json/features.h /usr/include/json/json-features.h && \
+ sed -i -e 's/features\.h/json-features\.h/g' /usr/include/json/*.h && \
+ cp -av /usr/include/json /usr/include/kmsjsoncpp && \
+ git clone https://github.com/Kurento/kms-jsonrpc.git /src/kms-jsonrpc.git && \
+ cd /src/kms-jsonrpc.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ .. && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ git clone https://github.com/Kurento/kms-core.git /src/kms-core.git && \
+ cd /src/kms-core.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ sed -r -i \
+ -e 's/gstreamer-(.*)1.5/gstreamer-\11.0/g' \
+ $(find -type f -name CMakeLists.txt) && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ -DCMAKE_INSTALL_GST_PLUGINS_DIR=/usr/lib64/gstreamer-1.0/ \
+ .. && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ git clone https://github.com/Kurento/kms-elements.git /src/kms-elements.git && \
+ cd /src/kms-elements.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ sed -r -i \
+ -e 's/gstreamer-(.*)1.5/gstreamer-\11.0/g' \
+ $(find -type f -name CMakeLists.txt) && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ .. && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ git clone https://github.com/Kurento/kms-filters.git /src/kms-filters.git && \
+ cd /src/kms-filters.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ sed -r -i \
+ -e 's/gstreamer-(.*)1.5/gstreamer-\11.0/g' \
+ $(find -type f -name CMakeLists.txt) && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ .. && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ git clone https://github.com/Kurento/kurento-media-server.git /src/kurento-media-server.git && \
+ cd /src/kurento-media-server.git/ && \
+ git checkout ${KURENTO_VERSION} && \
+ sed -r -i \
+ -e 's/gstreamer-(.*)1.5/gstreamer-\11.0/g' \
+ $(find -type f -name CMakeLists.txt) && \
+ sed -i \
+ -e '603,618d' \
+ server/death_handler.cpp && \
+ mkdir build && \
+ cd build && \
+ cmake \
+ ${CMAKEOPTS} \
+ .. && \
+ sed -i \
+ -e 's/-lkmsgstcommons/-lkmsgstcommons -lkmscoreinterface/g' \
+ server/CMakeFiles/kurento-media-server.dir/link.txt && \
+ make -j $(nproc) VERBOSE=1 && \
+ make install -s && \
+ cp -av $(find . -type f -name 'libtransport.so*') /usr/lib64/ && \
+ cp -av $(find . -type f -name 'libwebsocketTransport.so*') /usr/lib64/ && \
+ strip -s /usr/lib64/libkms*.so* /usr/lib64/kurento/modules/*.so* && \
+ dnf -y remove \
+ -x boost-filesystem \
+ -x boost-program-options \
+ -x boost-log \
+ -x boost-thread \
+ -x file \
+ -x gstreamer1-plugins-base \
+ -x gstreamer1-plugins-bad-free \
+ -x glibmm24 \
+ -x java-1.8.0-openjdk \
+ -x libnice \
+ -x libsigc \
+ -x libvpx \
+ -x opencv \
+ boost-devel \
+ cmake \
+ #ffmpeg \
+ gcc-c++ \
+ git \
+ glibmm24-devel \
+ gstreamer1-devel \
+ gstreamer1-plugins-base-devel \
+ gstreamer1-plugins-bad-free-devel \
+ gstreamer1-plugins-good \
+ libevent-devel \
+ libnice-devel \
+ libsigc++20-devel \
+ libsoup-devel \
+ libuuid-devel \
+ libvpx-devel \
+ make \
+ maven \
+ opencv-devel \
+ openssl-devel \
+ redhat-rpm-config \
+ vim-enhanced \
+ websocketpp-devel && \
+ dnf -y clean all && \
+ rm -rvf \
+ /src/ \
+ /root/.m2/ \
+ /usr/include/*
+
+CMD ["/bin/kurento-media-server"]
diff --git a/docker/openvidu/Dockerfile b/docker/openvidu/Dockerfile
new file mode 100644
--- /dev/null
+++ b/docker/openvidu/Dockerfile
@@ -0,0 +1,57 @@
+FROM fedora:31
+
+MAINTAINER Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>
+
+ENV CERTIFICATE_TYPE=selfsigned
+ENV DOMAIN_OR_PUBLIC_IP=localhost.localdomain
+ENV OPENVIDU_SECRET=MY_SECRET
+
+RUN dnf -y install \
+ --setopt 'tsflags=nodocs' \
+ bash-completion \
+ bind-utils \
+ coturn \
+ curl \
+ dhcp-client \
+ git \
+ iproute \
+ iptraf-ng \
+ iputils \
+ java-1.8.0-openjdk \
+ less \
+ lsof \
+ maven \
+ mtr \
+ net-tools \
+ NetworkManager \
+ NetworkManager-tui \
+ network-scripts \
+ nmap-ncat \
+ openssh-clients \
+ openssh-server \
+ procps-ng \
+ strace \
+ systemd-udev \
+ tcpdump \
+ telnet \
+ traceroute \
+ vim-enhanced \
+ wget && \
+ dnf clean all && \
+ mkdir -p /src/ && \
+ git clone --branch v2.15.0 https://github.com/OpenVidu/openvidu.git /src/openvidu.git && \
+ cd /src/openvidu.git/ && \
+ mvn clean install -DskipTests && \
+ mkdir -p /usr/share/openvidu/ && \
+ mv -v \
+ /src/openvidu.git/openvidu-server/target/openvidu-server-2.15.0.jar \
+ /usr/share/openvidu/openvidu-server.jar && \
+ rm -rvf /src/ /root/.m2/
+
+CMD [ \
+ "/usr/bin/java", \
+ "-Dserver.port=8080", \
+ "-Dserver.ssl.enabled=false", \
+ "-jar", \
+ "/usr/share/openvidu/openvidu-server.jar" \
+ ]
diff --git a/docker/proxy/Dockerfile b/docker/proxy/Dockerfile
new file mode 100644
--- /dev/null
+++ b/docker/proxy/Dockerfile
@@ -0,0 +1,46 @@
+FROM fedora:31
+
+MAINTAINER Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>
+
+ENV container docker
+ENV SYSTEMD_PAGER=''
+
+RUN dnf -y install \
+ --setopt 'tsflags=nodocs' \
+ bash-completion \
+ bind-utils \
+ certbot \
+ curl \
+ dhcp-client \
+ git \
+ iproute \
+ iptraf-ng \
+ iputils \
+ less \
+ lsof \
+ mtr \
+ net-tools \
+ NetworkManager \
+ NetworkManager-tui \
+ network-scripts \
+ nginx \
+ nmap-ncat \
+ openssh-clients \
+ openssh-server \
+ procps-ng \
+ python3-certbot-nginx \
+ strace \
+ systemd-udev \
+ tcpdump \
+ telnet \
+ traceroute \
+ vim-enhanced \
+ wget && \
+ dnf clean all
+
+COPY rootfs/ /
+
+RUN systemctl enable nginx
+
+CMD ["/lib/systemd/systemd", "--system"]
+ENTRYPOINT "/lib/systemd/systemd"
diff --git a/docker/proxy/rootfs/etc/nginx/conf.d/ssl.conf b/docker/proxy/rootfs/etc/nginx/conf.d/ssl.conf
new file mode 100644
--- /dev/null
+++ b/docker/proxy/rootfs/etc/nginx/conf.d/ssl.conf
@@ -0,0 +1,9 @@
+ssl_session_cache shared:le_nginx_SSL:10m;
+ssl_session_timeout 1440m;
+ssl_session_tickets off;
+
+ssl_protocols TLSv1.2 TLSv1.3;
+ssl_prefer_server_ciphers off;
+
+ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
+
diff --git a/docker/proxy/rootfs/etc/nginx/nginx.conf b/docker/proxy/rootfs/etc/nginx/nginx.conf
new file mode 100644
--- /dev/null
+++ b/docker/proxy/rootfs/etc/nginx/nginx.conf
@@ -0,0 +1,85 @@
+# For more information on configuration, see:
+# * Official English Documentation: http://nginx.org/en/docs/
+# * Official Russian Documentation: http://nginx.org/ru/docs/
+
+user nginx;
+worker_processes auto;
+error_log /var/log/nginx/error.log;
+pid /run/nginx.pid;
+
+# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
+include /usr/share/nginx/modules/*.conf;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+ }
+
+ # Load modular configuration files from the /etc/nginx/conf.d directory.
+ # See http://nginx.org/en/docs/ngx_core_module.html#include
+ # for more information.
+ include /etc/nginx/conf.d/*.conf;
+
+ server {
+ listen [::]:443 ssl ipv6only=on;
+ listen 443 ssl;
+
+ ssl_certificate /etc/letsencrypt/live/$server_name/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/$server_name/privkey.pem;
+
+ server_name kanarip.internet-box.ch;
+ root /usr/share/nginx/html;
+
+ # Load configuration files for the default server block.
+ include /etc/nginx/default.d/*.conf;
+
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_no_cache 1;
+ proxy_cache_bypass 1;
+ }
+
+ location /openvidu {
+ proxy_pass https://127.0.0.1:8443;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Host $host;
+ }
+
+ error_page 404 /404.html;
+ location = /40x.html {
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ }
+
+ }
+}
diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -60,6 +60,25 @@
LDAP_HOSTED_BIND_PW="Welcome2KolabSystems"
LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com"
+OPENVIDU_API_PASSWORD=MY_SECRET
+OPENVIDU_API_URL=http://localhost:8080/api/
+OPENVIDU_API_USERNAME=OPENVIDUAPP
+OPENVIDU_API_VERIFY_TLS=true
+OPENVIDU_COTURN_IP=127.0.0.1
+OPENVIDU_COTURN_REDIS_DATABASE=2
+OPENVIDU_COTURN_REDIS_IP=127.0.0.1
+OPENVIDU_COTURN_REDIS_PASSWORD=turn
+# Used as COTURN_IP, TURN_PUBLIC_IP, for KMS_TURN_URL
+OPENVIDU_PUBLIC_IP=127.0.0.1
+OPENVIDU_PUBLIC_PORT=3478
+OPENVIDU_SERVER_PORT=8080
+OPENVIDU_WEBHOOK=true
+OPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:8000/webhooks/meet/openvidu
+
+# "CDR" events, see https://docs.openvidu.io/en/2.13.0/reference-docs/openvidu-server-cdr/
+#OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged]
+#OPENVIDU_WEBHOOK_HEADERS=[\"Authorization:\ Basic\ SOMETHING\"]
+
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
diff --git a/src/app/Console/Commands/OpenVidu/RoomCreate.php b/src/app/Console/Commands/OpenVidu/RoomCreate.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/OpenVidu/RoomCreate.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Console\Commands\OpenVidu;
+
+use Illuminate\Console\Command;
+
+class RoomCreate extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'openvidu:room-create {user} {room}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Create a room for a user';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = \App\User::where('email', $this->argument('user'))->first();
+
+ if (!$user) {
+ return 1;
+ }
+
+ $roomName = $this->argument('room');
+
+ if (!preg_match('/^[a-zA-Z0-9_-]{1,16}$/', $roomName)) {
+ $this->error("Invalid room name. Should be up to 16 characters ([a-zA-Z0-9_-]).");
+ return 1;
+ }
+
+ $room = \App\OpenVidu\Room::where('name', $roomName)->first();
+
+ if ($room) {
+ $this->error("Room already exists.");
+ return 1;
+ }
+
+ \App\OpenVidu\Room::create(
+ [
+ 'name' => $roomName,
+ 'user_id' => $user->id
+ ]
+ );
+ }
+}
diff --git a/src/app/Console/Commands/OpenVidu/Rooms.php b/src/app/Console/Commands/OpenVidu/Rooms.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/OpenVidu/Rooms.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Console\Commands\OpenVidu;
+
+use Illuminate\Console\Command;
+
+class Rooms extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'openvidu:rooms';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'List OpenVidu rooms';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $rooms = \App\OpenVidu\Room::all();
+
+ foreach ($rooms as $room) {
+ $this->info("{$room->name}");
+ }
+ }
+}
diff --git a/src/app/Console/Commands/OpenVidu/Sessions.php b/src/app/Console/Commands/OpenVidu/Sessions.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/OpenVidu/Sessions.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Console\Commands\OpenVidu;
+
+use Illuminate\Console\Command;
+
+class Sessions extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'openvidu:sessions';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'List OpenVidu sessions';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ // curl -X GET -k -u OPENVIDUAPP:MY_SECRET https://localhost:4443/api/sessions, json
+
+ $client = new \GuzzleHttp\Client(
+ [
+ 'base_uri' => \config('openvidu.api_url'),
+ 'verify' => \config('openvidu.api_verify_tls')
+ ]
+ );
+
+ $response = $client->request(
+ 'GET',
+ 'sessions',
+ ['auth' => [\config('openvidu.api_username'), \config('openvidu.api_password')]]
+ );
+
+ if ($response->getStatusCode() !== 200) {
+ return 1;
+ }
+
+ $sessionResponse = json_decode($response->getBody(), true);
+
+ foreach ($sessionResponse['content'] as $session) {
+ $room = \App\OpenVidu\Room::where('session_id', $session['sessionId'])->first();
+ if ($room) {
+ $owner = $room->owner->email;
+ $roomName = $room->name;
+ } else {
+ $owner = '(none)';
+ $roomName = '(none)';
+ }
+
+ $this->info(
+ sprintf(
+ "Session: %s for %s since %s (by %s)",
+ $session['sessionId'],
+ $roomName,
+ \Carbon\Carbon::parse((int)substr($session['createdAt'], 0, 10), 'UTC'),
+ $owner
+ )
+ );
+ }
+ }
+}
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -96,6 +96,11 @@
return $this;
}
+ /**
+ * The domain entitlement.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\MorphOne
+ */
public function entitlement()
{
return $this->morphOne('App\Entitlement', 'entitleable');
diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/OpenViduController.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\Controller;
+use App\OpenVidu\Room;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Validator;
+
+class OpenViduController extends Controller
+{
+ /**
+ * Close the room session.
+ *
+ * @param string $id Room identifier (name)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function closeRoom($id)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->deleteSession()) {
+ return $this->errorResponse(500, \trans('meet.session-close-error'));
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => __('meet.session-close-success'),
+ ]);
+ }
+
+ /**
+ * Listing of rooms that belong to the current user.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = Auth::guard()->user();
+
+ $rooms = Room::where('user_id', $user->id)->orderBy('name')->get();
+
+ if (count($rooms) == 0) {
+ // Create a room for the user (with a random and unique name)
+ while (true) {
+ $name = \App\Utils::randStr(8);
+ if (!Room::where('name', $name)->count()) {
+ break;
+ }
+ }
+
+ $room = Room::create([
+ 'name' => $name,
+ 'user_id' => $user->id
+ ]);
+
+ $rooms = collect([$room]);
+ }
+
+ $result = [
+ 'list' => $rooms,
+ 'count' => count($rooms),
+ ];
+
+ return response()->json($result);
+ }
+
+ /**
+ * Join the room session. Each room has one owner, and the room isn't open until the owner
+ * joins (and effectively creates the session).
+ *
+ * @param string $id Room identifier (name)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function joinRoom($id)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // Room does not exist, or the owner is deleted
+ if (!$room || !$room->owner) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ // Check if there's still a valid beta entitlement for the room owner
+ $sku = \App\Sku::where('title', 'meet')->first();
+ if ($sku && !$room->owner->entitlements()->where('sku_id', $sku->id)->first()) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // There's no existing session
+ if (!$room->hasSession()) {
+ // Participants can't join the room until the session is created by the owner
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(423, \trans('meet.session-not-found'));
+ }
+
+ // The room owner can create the session on request
+ if (empty(request()->input('init'))) {
+ return $this->errorResponse(424, \trans('meet.session-not-found'));
+ }
+
+ $session = $room->createSession();
+
+ if (empty($session)) {
+ return $this->errorResponse(500, \trans('meet.session-create-error'));
+ }
+ }
+
+ // Create session token for the current user/connection
+ $response = $room->getSessionToken('PUBLISHER');
+
+ if (empty($response)) {
+ return $this->errorResponse(500, \trans('meet.session-join-error'));
+ }
+
+ // Create session token for screen sharing connection
+ if (!empty(request()->input('screenShare'))) {
+ $add_token = $room->getSessionToken('PUBLISHER');
+
+ $response['shareToken'] = $add_token['token'];
+ }
+
+ // Tell the UI who's the room owner
+ $response['owner'] = $user && $user->id == $room->user_id;
+
+ return response()->json($response);
+ }
+
+ /**
+ * Webhook as triggered from OpenVidu server
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\Response The response
+ */
+ public function webhook(Request $request)
+ {
+ \Log::debug($request->getContent());
+
+ switch ((string) $request->input('event')) {
+ case 'sessionDestroyed':
+ // When all participants left the room OpenVidu dispatches sessionDestroyed
+ // event. We'll remove the session reference from the database.
+ $sessionId = $request->input('sessionId');
+ $room = Room::where('session_id', $sessionId)->first();
+
+ if ($room) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ break;
+ }
+
+ return response('Success', 200);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -220,7 +220,17 @@
->where('entitleable_type', Domain::class)
->count() > 0;
+ // Get user's beta entitlements
+ $betaSKUs = $user->entitlements()->select('skus.title')
+ ->join('skus', 'skus.id', '=', 'entitlements.sku_id')
+ ->where('handler_class', 'like', 'App\\\\Handlers\\\\Beta\\\\%')
+ ->get()
+ ->pluck('title')
+ ->unique()
+ ->all();
+
return [
+ 'betaSKUs' => $betaSKUs,
// TODO: This will change when we enable all users to create domains
'enableDomains' => $isController && $hasCustomDomain,
'enableUsers' => $isController,
@@ -342,10 +352,17 @@
DB::commit();
- return response()->json([
- 'status' => 'success',
- 'message' => __('app.user-update-success'),
- ]);
+ $response = [
+ 'status' => 'success',
+ 'message' => __('app.user-update-success'),
+ ];
+
+ // For self-update refresh the statusInfo in the UI
+ if ($user->id == $current_user->id) {
+ $response['statusInfo'] = self::statusInfo($user);
+ }
+
+ return response()->json($response);
}
/**
diff --git a/src/app/Http/Kernel.php b/src/app/Http/Kernel.php
--- a/src/app/Http/Kernel.php
+++ b/src/app/Http/Kernel.php
@@ -21,6 +21,9 @@
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\DevelConfig::class,
+ // FIXME: CORS handling added here, I didn't find a nice way
+ // to add this only to the API routes
+ // \App\Http\Middleware\Cors::class,
];
/**
diff --git a/src/app/Http/Middleware/Cors.php b/src/app/Http/Middleware/Cors.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Middleware/Cors.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+
+class Cors
+{
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ *
+ * @return mixed
+ */
+ public function handle($request, Closure $next)
+ {
+ // TODO: Now we allow all, but we should be allowing only meet.domain.tld
+
+ return $next($request)
+ ->header('Access-Control-Allow-Origin', '*')
+ ->header(
+ 'Access-Control-Allow-Methods',
+ 'GET, POST, PUT, DELETE, OPTIONS'
+ )
+ ->header(
+ 'Access-Control-Allow-Headers',
+ 'X-Requested-With, Content-Type, X-Token-Auth, Authorization'
+ );
+ }
+}
diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php
new file mode 100644
--- /dev/null
+++ b/src/app/OpenVidu/Room.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace App\OpenVidu;
+
+use App\Traits\SettingsTrait;
+use Illuminate\Database\Eloquent\Model;
+
+class Room extends Model
+{
+ use SettingsTrait;
+
+ protected $fillable = [
+ 'user_id',
+ 'name'
+ ];
+
+ protected $table = 'openvidu_rooms';
+
+ /** @var \GuzzleHttp\Client|null HTTP client instance */
+ private static $client = null;
+
+
+ /**
+ * Creates HTTP client for connections to OpenVidu server
+ *
+ * @return \GuzzleHttp\Client HTTP client instance
+ */
+ private function client()
+ {
+ if (!self::$client) {
+ self::$client = new \GuzzleHttp\Client(
+ [
+ 'http_errors' => false, // No exceptions from Guzzle
+ 'base_uri' => \config('openvidu.api_url'),
+ 'verify' => \config('openvidu.api_verify_tls'),
+ 'auth' => [
+ \config('openvidu.api_username'),
+ \config('openvidu.api_password')
+ ]
+ ]
+ );
+ }
+
+ return self::$client;
+ }
+
+ /**
+ * Create a OpenVidu session
+ *
+ * @return array|null Session data on success, NULL otherwise
+ */
+ public function createSession(): ?array
+ {
+ $response = $this->client()->request(
+ 'POST',
+ "sessions",
+ [
+ 'json' => [
+ 'mediaMode' => 'ROUTED',
+ 'recordingMode' => 'MANUAL'
+ ]
+ ]
+ );
+
+ if ($response->getStatusCode() !== 200) {
+ $this->session_id = null;
+ $this->save();
+ }
+
+ $session = json_decode($response->getBody(), true);
+
+ $this->session_id = $session['id'];
+ $this->save();
+
+ return $session;
+ }
+
+ /**
+ * Delete a OpenVidu session
+ *
+ * @return bool
+ */
+ public function deleteSession(): bool
+ {
+ if (!$this->session_id) {
+ return true;
+ }
+
+ $response = $this->client()->request(
+ 'DELETE',
+ "sessions/" . $this->session_id,
+ );
+
+ if ($response->getStatusCode() == 204) {
+ $this->session_id = null;
+ $this->save();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create a OpenVidu session (connection) token
+ *
+ * @return array|null Token data on success, NULL otherwise
+ */
+ public function getSessionToken($role = 'PUBLISHER'): ?array
+ {
+ $response = $this->client()->request(
+ 'POST',
+ 'tokens',
+ [
+ 'json' => [
+ 'session' => $this->session_id,
+ 'role' => $role
+ ]
+ ]
+ );
+
+ if ($response->getStatusCode() == 200) {
+ $json = json_decode($response->getBody(), true);
+
+ return $json;
+ }
+
+ return null;
+ }
+
+ /**
+ * Check if the room has an active session
+ *
+ * @return bool True when the session exists, False otherwise
+ */
+ public function hasSession(): bool
+ {
+ if (!$this->session_id) {
+ return false;
+ }
+
+ $response = $this->client()->request('GET', "sessions/{$this->session_id}");
+
+ return $response->getStatusCode() == 200;
+ }
+
+ /**
+ * The room owner.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function owner()
+ {
+ return $this->belongsTo('\App\User', 'user_id', 'id');
+ }
+
+ /**
+ * Any (additional) properties of this room.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function settings()
+ {
+ return $this->hasMany('App\OpenVidu\RoomSetting', 'room_id');
+ }
+}
diff --git a/src/app/OpenVidu/RoomSetting.php b/src/app/OpenVidu/RoomSetting.php
new file mode 100644
--- /dev/null
+++ b/src/app/OpenVidu/RoomSetting.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace App\OpenVidu;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * A collection of settings for a Room.
+ *
+ * @property int $id
+ * @property int $room_id
+ * @property string $key
+ * @property string $value
+ */
+class RoomSetting extends Model
+{
+ protected $fillable = [
+ 'room_id', 'key', 'value'
+ ];
+
+ protected $table = 'openvidu_room_settings';
+
+ /**
+ * The user to which this setting belongs.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function room()
+ {
+ return $this->belongsTo(
+ '\App\OpenVidu\Room',
+ 'room_id', /* local */
+ 'id' /* remote */
+ );
+ }
+}
diff --git a/src/app/Package.php b/src/app/Package.php
--- a/src/app/Package.php
+++ b/src/app/Package.php
@@ -80,6 +80,11 @@
return false;
}
+ /**
+ * SKUs of this package.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ */
public function skus()
{
return $this->belongsToMany(
diff --git a/src/app/SignupCode.php b/src/app/SignupCode.php
--- a/src/app/SignupCode.php
+++ b/src/app/SignupCode.php
@@ -15,8 +15,6 @@
*/
class SignupCode extends Model
{
- // Note: Removed '0', 'O', '1', 'I' as problematic with some fonts
- public const SHORTCODE_CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
public const SHORTCODE_LENGTH = 5;
public const CODE_LENGTH = 32;
@@ -92,15 +90,7 @@
public static function generateShortCode(): string
{
$code_length = env('SIGNUP_CODE_LENGTH', self::SHORTCODE_LENGTH);
- $code_chars = env('SIGNUP_CODE_CHARS', self::SHORTCODE_CHARS);
- $random = [];
- for ($i = 1; $i <= $code_length; $i++) {
- $random[] = $code_chars[rand(0, strlen($code_chars) - 1)];
- }
-
- shuffle($random);
-
- return implode('', $random);
+ return \App\Utils::randStr($code_length);
}
}
diff --git a/src/app/Sku.php b/src/app/Sku.php
--- a/src/app/Sku.php
+++ b/src/app/Sku.php
@@ -47,6 +47,11 @@
return $this->hasMany('App\Entitlement');
}
+ /**
+ * List of packages that use this SKU.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ */
public function packages()
{
return $this->belongsToMany(
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -318,6 +318,11 @@
return $domains;
}
+ /**
+ * The user entitlement.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\MorphOne
+ */
public function entitlement()
{
return $this->morphOne('App\Entitlement', 'entitleable');
@@ -333,7 +338,8 @@
*/
public function entitlements()
{
- return $this->hasMany('App\Entitlement', 'entitleable_id', 'id');
+ return $this->hasMany('App\Entitlement', 'entitleable_id', 'id')
+ ->where('entitleable_type', User::class);
}
public function addEntitlement($entitlement)
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -11,6 +11,9 @@
*/
class Utils
{
+ // Note: Removed '0', 'O', '1', 'I' as problematic with some fonts
+ public const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
+
/**
* Count the number of lines in a file.
*
@@ -245,6 +248,43 @@
}
/**
+ * Returns a random string consisting of a quantity of segments of a certain length joined.
+ *
+ * Example:
+ *
+ * ```php
+ * $roomName = strtolower(\App\Utils::randStr(3, 3, '-');
+ * // $roomName == '3qb-7cs-cjj'
+ * ```
+ *
+ * @param int $length The length of each segment
+ * @param int $qty The quantity of segments
+ * @param string $join The string to use to join the segments
+ *
+ * @return string
+ */
+ public static function randStr($length, $qty = 1, $join = '')
+ {
+ $chars = env('SHORTCODE_CHARS', self::CHARS);
+
+ $randStrs = [];
+
+ for ($x = 0; $x < $qty; $x++) {
+ $randStrs[$x] = [];
+
+ for ($y = 0; $y < $length; $y++) {
+ $randStrs[$x][] = $chars[rand(0, strlen($chars) - 1)];
+ }
+
+ shuffle($randStrs[$x]);
+
+ $randStrs[$x] = implode('', $randStrs[$x]);
+ }
+
+ return implode($join, $randStrs);
+ }
+
+ /**
* Returns a UUID in the form of an integer.
*
* @return integer
@@ -297,6 +337,7 @@
* Create self URL
*
* @param string $route Route/Path
+ * @todo Move this to App\Http\Controllers\Controller
*
* @return string Full URL
*/
@@ -315,12 +356,16 @@
* Create a configuration/environment data to be passed to
* the UI
*
- * @todo For a lack of better place this is put here for now
+ * @todo Move this to App\Http\Controllers\Controller
*
* @return array Configuration data
*/
public static function uiEnv(): array
{
+ $countries = include resource_path('countries.php');
+ $req_domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost());
+ $sys_domain = \config('app.domain');
+ $path = request()->path();
$opts = [
'app.name',
'app.url',
@@ -333,11 +378,16 @@
$env = \app('config')->getMany($opts);
- $countries = include resource_path('countries.php');
$env['countries'] = $countries ?: [];
-
- $isAdmin = strpos(request()->getHttpHost(), 'admin.') === 0;
- $env['jsapp'] = $isAdmin ? 'admin.js' : 'user.js';
+ $env['view'] = 'root';
+ $env['jsapp'] = 'user.js';
+
+ if ($path == 'meet' || strpos($path, 'meet/') === 0) {
+ $env['view'] = 'meet';
+ $env['jsapp'] = 'meet.js';
+ } elseif ($req_domain == "admin.$sys_domain") {
+ $env['jsapp'] = 'admin.js';
+ }
$env['paymentProvider'] = \config('services.payment_provider');
$env['stripePK'] = \config('services.stripe.public_key');
diff --git a/src/app/VerificationCode.php b/src/app/VerificationCode.php
--- a/src/app/VerificationCode.php
+++ b/src/app/VerificationCode.php
@@ -3,7 +3,6 @@
namespace App;
use App\SignupCode;
-use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
@@ -51,15 +50,7 @@
public static function generateShortCode(): string
{
$code_length = env('VERIFICATION_CODE_LENGTH', self::SHORTCODE_LENGTH);
- $code_chars = env('VERIFICATION_CODE_CHARS', self::SHORTCODE_CHARS);
- $random = [];
- for ($i = 1; $i <= $code_length; $i++) {
- $random[] = $code_chars[rand(0, strlen($code_chars) - 1)];
- }
-
- shuffle($random);
-
- return implode('', $random);
+ return \App\Utils::randStr($code_length);
}
}
diff --git a/src/config/openvidu.php b/src/config/openvidu.php
new file mode 100644
--- /dev/null
+++ b/src/config/openvidu.php
@@ -0,0 +1,7 @@
+<?php
+ return [
+ 'api_password' => env('OPENVIDU_API_PASSWORD', 'MY_SECRET'),
+ 'api_url' => env('OPENVIDU_API_URL', 'https://localhost:8443/api/'),
+ 'api_username' => env('OPENVIDU_API_USERNAME', 'OPENVIDUAPP'),
+ 'api_verify_tls' => (bool) env('OPENVIDU_API_VERIFY_TLS', true)
+ ];
diff --git a/src/config/session.php b/src/config/session.php
--- a/src/config/session.php
+++ b/src/config/session.php
@@ -126,7 +126,7 @@
'cookie' => env(
'SESSION_COOKIE',
- Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
+ Str::slug(env('APP_NAME', 'laravel'), '_') . '_session'
),
/*
diff --git a/src/database/migrations/2020_04_30_115440_create_openvidu_tables.php b/src/database/migrations/2020_04_30_115440_create_openvidu_tables.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_04_30_115440_create_openvidu_tables.php
@@ -0,0 +1,55 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateOpenviduTables extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('user_id');
+ $table->string('name', 16)->unique();
+ $table->string('session_id', 16)->nullable()->unique();
+ $table->timestamps();
+
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ }
+ );
+
+ Schema::create(
+ 'openvidu_room_settings',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('room_id')->unsigned();
+ $table->string('key', 16);
+ $table->string('value');
+ $table->timestamps();
+
+ $table->foreign('room_id')->references('id')
+ ->on('openvidu_rooms')->onDelete('cascade');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('openvidu_room_settings');
+ Schema::dropIfExists('openvidu_rooms');
+ }
+}
diff --git a/src/database/migrations/2020_10_29_100000_add_beta_skus.php b/src/database/migrations/2020_10_29_100000_add_beta_skus.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_10_29_100000_add_beta_skus.php
@@ -0,0 +1,53 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+// phpcs:ignore
+class AddBetaSkus extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ if (!\App\Sku::where('title', 'beta')->first()) {
+ \App\Sku::create([
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]);
+ }
+
+ if (!\App\Sku::where('title', 'meet')->first()) {
+ \App\Sku::create([
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]);
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // there's no need to remove these SKUs
+ }
+}
diff --git a/src/database/seeds/DatabaseSeeder.php b/src/database/seeds/DatabaseSeeder.php
--- a/src/database/seeds/DatabaseSeeder.php
+++ b/src/database/seeds/DatabaseSeeder.php
@@ -19,7 +19,8 @@
'SkuSeeder',
'PackageSeeder',
'PlanSeeder',
- 'UserSeeder'
+ 'UserSeeder',
+ 'OpenViduRoomSeeder',
];
$env = ucfirst(App::environment());
diff --git a/src/database/seeds/local/OpenViduRoomSeeder.php b/src/database/seeds/local/OpenViduRoomSeeder.php
new file mode 100644
--- /dev/null
+++ b/src/database/seeds/local/OpenViduRoomSeeder.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Database\Seeds\Local;
+
+use App\OpenVidu\Room;
+use Illuminate\Database\Seeder;
+
+class OpenViduRoomSeeder extends Seeder
+{
+ /**
+ * Run the database seeds.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ $john = \App\User::where('email', 'john@kolab.org')->first();
+ $jack = \App\User::where('email', 'jack@kolab.org')->first();
+
+ \App\OpenVidu\Room::create(
+ [
+ 'user_id' => $john->id,
+ 'name' => 'john'
+ ]
+ );
+
+ \App\OpenVidu\Room::create(
+ [
+ 'user_id' => $jack->id,
+ 'name' => strtolower(\App\Utils::randStr(3, 3, '-'))
+ ]
+ );
+ }
+}
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -152,30 +152,36 @@
]
);
- Sku::create(
- [
- 'title' => 'beta',
- 'name' => 'Beta program',
- 'description' => 'Access to beta program subscriptions',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta',
- 'active' => false,
- ]
- );
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'beta')->first()) {
+ Sku::create(
+ [
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]
+ );
+ }
- Sku::create(
- [
- 'title' => 'meet',
- 'name' => 'Video chat',
- 'description' => 'Video conferencing tool',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta\Meet',
- 'active' => true,
- ]
- );
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'meet')->first()) {
+ Sku::create(
+ [
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]
+ );
+ }
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -152,30 +152,36 @@
]
);
- Sku::create(
- [
- 'title' => 'beta',
- 'name' => 'Beta program',
- 'description' => 'Access to beta program subscriptions',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta',
- 'active' => false,
- ]
- );
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'beta')->first()) {
+ Sku::create(
+ [
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]
+ );
+ }
- Sku::create(
- [
- 'title' => 'meet',
- 'name' => 'Video chat',
- 'description' => 'Video conferencing tool',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta\Meet',
- 'active' => true,
- ]
- );
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'meet')->first()) {
+ Sku::create(
+ [
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]
+ );
+ }
}
}
diff --git a/src/package-lock.json b/src/package-lock.json
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -3585,6 +3585,12 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true
},
+ "anchorme": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/anchorme/-/anchorme-2.1.2.tgz",
+ "integrity": "sha512-2iPY3kxDDZvtRzauqKDb4v7a5sTF4GZ+esQTY8nGYvmhAtGTeFPMn4cRnvyWS1qmtPTP0Mv8hyLOp9l3ZzWMKg==",
+ "dev": true
+ },
"ansi-colors": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
@@ -3716,26 +3722,6 @@
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
"dev": true
},
- "asn1.js": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
- "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
- "dev": true,
- "requires": {
- "bn.js": "^4.0.0",
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0",
- "safer-buffer": "^2.1.0"
- },
- "dependencies": {
- "bn.js": {
- "version": "4.11.9",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
- "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
- "dev": true
- }
- }
- },
"assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
@@ -3824,13 +3810,12 @@
}
},
"axios": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
- "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"dev": true,
"requires": {
- "follow-redirects": "1.5.10",
- "is-buffer": "^2.0.2"
+ "follow-redirects": "1.5.10"
}
},
"babel-code-frame": {
@@ -9559,252 +9544,4667 @@
"mimic-fn": "^2.1.0"
}
},
- "opn": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
- "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==",
- "dev": true,
- "requires": {
- "is-wsl": "^1.1.0"
- }
- },
- "optimize-css-assets-webpack-plugin": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz",
- "integrity": "sha512-wqd6FdI2a5/FdoiCNNkEvLeA//lHHfG24Ln2Xm2qqdIk4aOlsR18jwpyOihqQ8849W3qu2DX8fOYxpvTMj+93A==",
- "dev": true,
- "requires": {
- "cssnano": "^4.1.10",
- "last-call-webpack-plugin": "^3.0.0"
- }
- },
- "optionator": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
- "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
- "dev": true,
- "requires": {
- "deep-is": "~0.1.3",
- "fast-levenshtein": "~2.0.6",
- "levn": "~0.3.0",
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2",
- "word-wrap": "~1.2.3"
- }
- },
- "original": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
- "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
- "dev": true,
- "requires": {
- "url-parse": "^1.4.3"
- }
- },
- "os-browserify": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
- "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
- "dev": true
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
- },
- "p-finally": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
- "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
- "dev": true
- },
- "p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
- "requires": {
- "p-try": "^2.0.0"
- }
- },
- "p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "dev": true,
- "requires": {
- "p-limit": "^2.0.0"
- }
- },
- "p-map": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
- "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
- "dev": true,
- "requires": {
- "aggregate-error": "^3.0.0"
- }
- },
- "p-pipe": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz",
- "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=",
- "dev": true
- },
- "p-retry": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
- "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
- "dev": true,
- "requires": {
- "retry": "^0.12.0"
- }
- },
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true
- },
- "pako": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true
- },
- "parallel-transform": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
- "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
- "dev": true,
- "requires": {
- "cyclist": "^1.0.1",
- "inherits": "^2.0.3",
- "readable-stream": "^2.1.5"
- }
- },
- "param-case": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
- "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
- "dev": true,
- "requires": {
- "no-case": "^2.2.0"
- }
- },
- "parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "requires": {
- "callsites": "^3.0.0"
- }
- },
- "parse-asn1": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
- "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
- "dev": true,
- "requires": {
- "asn1.js": "^5.2.0",
- "browserify-aes": "^1.0.0",
- "evp_bytestokey": "^1.0.0",
- "pbkdf2": "^3.0.3",
- "safe-buffer": "^5.1.1"
- }
- },
- "parse-entities": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
- "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
- "dev": true,
- "requires": {
- "character-entities": "^1.0.0",
- "character-entities-legacy": "^1.0.0",
- "character-reference-invalid": "^1.0.0",
- "is-alphanumerical": "^1.0.0",
- "is-decimal": "^1.0.0",
- "is-hexadecimal": "^1.0.0"
- }
- },
- "parse-json": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
- "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "openvidu-browser": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/openvidu-browser/-/openvidu-browser-2.15.0.tgz",
+ "integrity": "sha512-agnyeYIf1ze5ynGqNw32zFedlov9JZzjoFQHNMwuAoFYc2/24Aajs9cyw3j0m7v8xmMkqWSOYvsu7kGc8z1mZg==",
"dev": true,
"requires": {
- "error-ex": "^1.3.1",
- "json-parse-better-errors": "^1.0.1"
- }
- },
- "parse-passwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
- "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
- "dev": true
- },
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "dev": true
- },
- "pascalcase": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
- "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
- "dev": true
- },
- "path-browserify": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
- "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==",
- "dev": true
- },
- "path-dirname": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
- "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
- "dev": true
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "dev": true
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
- },
- "path-is-inside": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
- "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
- "dev": true
- },
- "path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true
- },
- "path-parse": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
- "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
- "dev": true
- },
- "path-to-regexp": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
- "dev": true
- },
+ "@types/node": "13.13.2",
+ "@types/platform": "1.3.2",
+ "freeice": "2.2.2",
+ "hark": "1.2.3",
+ "platform": "1.3.5",
+ "uuid": "7.0.3",
+ "wolfy87-eventemitter": "5.2.9"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+ "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+ "requires": {
+ "@babel/highlight": "^7.8.3"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.9.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz",
+ "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g=="
+ },
+ "@babel/highlight": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
+ "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.9.0",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/node": {
+ "version": "13.13.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.2.tgz",
+ "integrity": "sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==",
+ "dev": true
+ },
+ "@types/platform": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.2.tgz",
+ "integrity": "sha512-Tn6OuJDAG7bJbyi4R7HqcxXp1w2lmIxVXqyNhPt1Bm0FO2EWIi3CI87JVzF7ncqK0ZMPuUycS3wTMIk85EeF1Q==",
+ "dev": true
+ },
+ "JSONStream": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
+ "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "requires": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ }
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "acorn": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+ "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg=="
+ },
+ "acorn-node": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
+ "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
+ "requires": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "acorn-walk": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz",
+ "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ=="
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
+ },
+ "anymatch": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
+ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
+ "requires": {
+ "micromatch": "^3.1.4",
+ "normalize-path": "^2.1.1"
+ },
+ "dependencies": {
+ "normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+ "requires": {
+ "remove-trailing-separator": "^1.0.1"
+ }
+ }
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ },
+ "dependencies": {
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ }
+ }
+ },
+ "arr-diff": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+ "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA="
+ },
+ "arr-flatten": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg=="
+ },
+ "arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ="
+ },
+ "array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8="
+ },
+ "array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
+ },
+ "array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w=="
+ },
+ "array-unique": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg="
+ },
+ "asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "assert": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
+ "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
+ "requires": {
+ "object-assign": "^4.1.1",
+ "util": "0.10.3"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
+ },
+ "util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+ "requires": {
+ "inherits": "2.0.1"
+ }
+ }
+ }
+ },
+ "assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
+ },
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
+ },
+ "async-each": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
+ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ=="
+ },
+ "atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ },
+ "base": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+ "requires": {
+ "cache-base": "^1.0.1",
+ "class-utils": "^0.3.5",
+ "component-emitter": "^1.2.1",
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.1",
+ "mixin-deep": "^1.2.0",
+ "pascalcase": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "base64-js": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+ },
+ "binary-extensions": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
+ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw=="
+ },
+ "bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "optional": true,
+ "requires": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "bn.js": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+ },
+ "body": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz",
+ "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=",
+ "requires": {
+ "continuable-cache": "^0.3.1",
+ "error": "^7.0.0",
+ "raw-body": "~1.1.0",
+ "safe-json-parse": "~1.0.1"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+ "requires": {
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+ },
+ "browser-pack": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz",
+ "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "combine-source-map": "~0.8.0",
+ "defined": "^1.0.0",
+ "safe-buffer": "^5.1.1",
+ "through2": "^2.0.0",
+ "umd": "^3.0.0"
+ }
+ },
+ "browser-resolve": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
+ "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
+ "requires": {
+ "resolve": "1.1.7"
+ },
+ "dependencies": {
+ "resolve": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+ }
+ }
+ },
+ "browserify": {
+ "version": "16.5.1",
+ "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.5.1.tgz",
+ "integrity": "sha512-EQX0h59Pp+0GtSRb5rL6OTfrttlzv+uyaUVlK6GX3w11SQ0jKPKyjC/54RhPR2ib2KmfcELM06e8FxcI5XNU2A==",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "assert": "^1.4.0",
+ "browser-pack": "^6.0.1",
+ "browser-resolve": "^1.11.0",
+ "browserify-zlib": "~0.2.0",
+ "buffer": "~5.2.1",
+ "cached-path-relative": "^1.0.0",
+ "concat-stream": "^1.6.0",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "~1.0.0",
+ "crypto-browserify": "^3.0.0",
+ "defined": "^1.0.0",
+ "deps-sort": "^2.0.0",
+ "domain-browser": "^1.2.0",
+ "duplexer2": "~0.1.2",
+ "events": "^2.0.0",
+ "glob": "^7.1.0",
+ "has": "^1.0.0",
+ "htmlescape": "^1.1.0",
+ "https-browserify": "^1.0.0",
+ "inherits": "~2.0.1",
+ "insert-module-globals": "^7.0.0",
+ "labeled-stream-splicer": "^2.0.0",
+ "mkdirp-classic": "^0.5.2",
+ "module-deps": "^6.0.0",
+ "os-browserify": "~0.3.0",
+ "parents": "^1.0.1",
+ "path-browserify": "~0.0.0",
+ "process": "~0.11.0",
+ "punycode": "^1.3.2",
+ "querystring-es3": "~0.2.0",
+ "read-only-stream": "^2.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.1.4",
+ "shasum": "^1.0.0",
+ "shell-quote": "^1.6.1",
+ "stream-browserify": "^2.0.0",
+ "stream-http": "^3.0.0",
+ "string_decoder": "^1.1.1",
+ "subarg": "^1.0.0",
+ "syntax-error": "^1.1.1",
+ "through2": "^2.0.0",
+ "timers-browserify": "^1.0.1",
+ "tty-browserify": "0.0.1",
+ "url": "~0.11.0",
+ "util": "~0.10.1",
+ "vm-browserify": "^1.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "requires": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "requires": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "browserify-rsa": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "browserify-sign": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+ "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+ "requires": {
+ "bn.js": "^4.1.1",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.2",
+ "elliptic": "^6.0.0",
+ "inherits": "^2.0.1",
+ "parse-asn1": "^5.0.0"
+ }
+ },
+ "browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "requires": {
+ "pako": "~1.0.5"
+ }
+ },
+ "buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
+ "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
+ "requires": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+ },
+ "buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+ },
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
+ },
+ "builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
+ },
+ "bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz",
+ "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g="
+ },
+ "cache-base": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+ "requires": {
+ "collection-visit": "^1.0.0",
+ "component-emitter": "^1.2.1",
+ "get-value": "^2.0.6",
+ "has-value": "^1.0.0",
+ "isobject": "^3.0.1",
+ "set-value": "^2.0.0",
+ "to-object-path": "^0.3.0",
+ "union-value": "^1.0.0",
+ "unset-value": "^1.0.0"
+ }
+ },
+ "cached-path-relative": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
+ "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg=="
+ },
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+ },
+ "camelcase-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+ "requires": {
+ "camelcase": "^2.0.0",
+ "map-obj": "^1.0.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chokidar": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
+ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
+ "requires": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "fsevents": "^1.2.7",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ },
+ "dependencies": {
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ }
+ }
+ },
+ "cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "class-utils": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+ "requires": {
+ "arr-union": "^3.1.0",
+ "define-property": "^0.2.5",
+ "isobject": "^3.0.0",
+ "static-extend": "^0.1.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "coffeescript": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz",
+ "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4="
+ },
+ "collection-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+ "requires": {
+ "map-visit": "^1.0.0",
+ "object-visit": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "colors": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM="
+ },
+ "combine-source-map": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz",
+ "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=",
+ "requires": {
+ "convert-source-map": "~1.1.0",
+ "inline-source-map": "~0.6.0",
+ "lodash.memoize": "~3.0.3",
+ "source-map": "~0.5.3"
+ }
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
+ "component-emitter": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
+ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "console-browserify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="
+ },
+ "constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
+ },
+ "continuable-cache": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz",
+ "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8="
+ },
+ "convert-source-map": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz",
+ "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA="
+ },
+ "copy-descriptor": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "create-ecdh": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+ "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.0.0"
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "cross-spawn": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-0.2.9.tgz",
+ "integrity": "sha1-vWf5bAfvtjA7f+lMHpefiEeOCjk=",
+ "requires": {
+ "lru-cache": "^2.5.0"
+ }
+ },
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
+ "csproj2ts": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-1.1.0.tgz",
+ "integrity": "sha512-sk0RTT51t4lUNQ7UfZrqjQx7q4g0m3iwNA6mvyh7gLsgQYvwKzfdyoAgicC9GqJvkoIkU0UmndV9c7VZ8pJ45Q==",
+ "requires": {
+ "es6-promise": "^4.1.1",
+ "lodash": "^4.17.4",
+ "semver": "^5.4.1",
+ "xml2js": "^0.4.19"
+ },
+ "dependencies": {
+ "es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
+ }
+ }
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "dargs": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz",
+ "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "dash-ast": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz",
+ "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA=="
+ },
+ "dateformat": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+ "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+ "requires": {
+ "get-stdin": "^4.0.1",
+ "meow": "^3.3.0"
+ }
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
+ },
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "dependencies": {
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
+ },
+ "deps-sort": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz",
+ "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "shasum-object": "^1.0.0",
+ "subarg": "^1.0.0",
+ "through2": "^2.0.0"
+ }
+ },
+ "des.js": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
+ "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc="
+ },
+ "detect-indent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "detect-newline": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz",
+ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I="
+ },
+ "detective": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
+ "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
+ "requires": {
+ "acorn-node": "^1.6.1",
+ "defined": "^1.0.0",
+ "minimist": "^1.1.1"
+ }
+ },
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "domain-browser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA=="
+ },
+ "duplexer": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E="
+ },
+ "duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+ "requires": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "elliptic": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
+ "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+ "requires": {
+ "bn.js": "^4.4.0",
+ "brorand": "^1.0.1",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.0"
+ }
+ },
+ "error": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz",
+ "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==",
+ "requires": {
+ "string-template": "~0.2.1"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es6-promise": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz",
+ "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "eventemitter2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+ "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas="
+ },
+ "events": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz",
+ "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg=="
+ },
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw="
+ },
+ "expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "requires": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+ "requires": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "requires": {
+ "array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
+ "extend-shallow": "^2.0.1",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "fast-safe-stringify": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
+ "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
+ },
+ "faye-websocket": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+ "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+ "requires": {
+ "websocket-driver": ">=0.5.1"
+ }
+ },
+ "figures": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+ "requires": {
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "file-sync-cmp": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz",
+ "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs="
+ },
+ "file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "optional": true
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "requires": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "findup-sync": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+ "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
+ "requires": {
+ "glob": "~5.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ }
+ },
+ "flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q=="
+ },
+ "for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
+ },
+ "for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+ "requires": {
+ "for-in": "^1.0.1"
+ }
+ },
+ "fragment-cache": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+ "requires": {
+ "map-cache": "^0.2.2"
+ }
+ },
+ "freeice": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/freeice/-/freeice-2.2.2.tgz",
+ "integrity": "sha512-XNoIxDHufqPIBSLpp4IrFPnoc+hv/0RwdOGhIoggIDC2ZKf5r6OoixbeoFJSmZOAq2aYiEUArhuQ8zVVrM5C4w==",
+ "dev": true,
+ "requires": {
+ "normalice": "^1.0.0"
+ }
+ },
+ "fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "requires": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "fsevents": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz",
+ "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==",
+ "optional": true,
+ "requires": {
+ "bindings": "^1.5.0",
+ "nan": "^2.12.1",
+ "node-pre-gyp": "*"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true,
+ "optional": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true,
+ "optional": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true,
+ "optional": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "1.1.4",
+ "bundled": true,
+ "optional": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true,
+ "optional": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true,
+ "optional": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true
+ },
+ "debug": {
+ "version": "3.2.6",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "bundled": true,
+ "optional": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true,
+ "optional": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "bundled": true,
+ "optional": true
+ },
+ "fs-minipass": {
+ "version": "1.2.7",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.6.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true,
+ "optional": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.6",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore-walk": {
+ "version": "3.0.3",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "bundled": true,
+ "optional": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "bundled": true,
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "bundled": true,
+ "optional": true
+ },
+ "minipass": {
+ "version": "2.9.0",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.3.3",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.9.0"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.3",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "bundled": true,
+ "optional": true
+ },
+ "needle": {
+ "version": "2.3.3",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "debug": "^3.2.6",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-pre-gyp": {
+ "version": "0.14.0",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.1",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.2.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4.4.2"
+ }
+ },
+ "nopt": {
+ "version": "4.0.3",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.1.1",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "npm-normalize-package-bin": "^1.0.1"
+ }
+ },
+ "npm-normalize-package-bin": {
+ "version": "1.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "npm-packlist": {
+ "version": "1.4.8",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1",
+ "npm-normalize-package-bin": "^1.0.1"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "rc": {
+ "version": "1.2.8",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true,
+ "optional": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true,
+ "optional": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "bundled": true,
+ "optional": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "bundled": true,
+ "optional": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true,
+ "optional": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true,
+ "optional": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "bundled": true,
+ "optional": true
+ },
+ "tar": {
+ "version": "4.4.13",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.8.6",
+ "minizlib": "^1.2.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.3"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true
+ },
+ "yallist": {
+ "version": "3.1.1",
+ "bundled": true,
+ "optional": true
+ }
+ }
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "gaze": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+ "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+ "requires": {
+ "globule": "^1.0.0"
+ }
+ },
+ "get-assigned-identifiers": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
+ "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ=="
+ },
+ "get-stdin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
+ },
+ "get-value": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg="
+ },
+ "getobject": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz",
+ "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw="
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
+ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
+ "requires": {
+ "is-glob": "^3.1.0",
+ "path-dirname": "^1.0.0"
+ }
+ },
+ "global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "requires": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ }
+ },
+ "global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+ "requires": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ }
+ },
+ "globule": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz",
+ "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==",
+ "requires": {
+ "glob": "~7.1.1",
+ "lodash": "~4.17.12",
+ "minimatch": "~3.0.2"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
+ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
+ },
+ "grunt": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.1.0.tgz",
+ "integrity": "sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g==",
+ "requires": {
+ "coffeescript": "~1.10.0",
+ "dateformat": "~1.0.12",
+ "eventemitter2": "~0.4.13",
+ "exit": "~0.1.1",
+ "findup-sync": "~0.3.0",
+ "glob": "~7.0.0",
+ "grunt-cli": "~1.2.0",
+ "grunt-known-options": "~1.1.0",
+ "grunt-legacy-log": "~2.0.0",
+ "grunt-legacy-util": "~1.1.1",
+ "iconv-lite": "~0.4.13",
+ "js-yaml": "~3.13.1",
+ "minimatch": "~3.0.2",
+ "mkdirp": "~1.0.3",
+ "nopt": "~3.0.6",
+ "path-is-absolute": "~1.0.0",
+ "rimraf": "~2.6.2"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
+ "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.2",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "grunt-cli": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz",
+ "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=",
+ "requires": {
+ "findup-sync": "~0.3.0",
+ "grunt-known-options": "~1.1.0",
+ "nopt": "~3.0.6",
+ "resolve": "~1.1.0"
+ }
+ },
+ "resolve": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+ }
+ }
+ },
+ "grunt-cli": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz",
+ "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==",
+ "requires": {
+ "grunt-known-options": "~1.1.0",
+ "interpret": "~1.1.0",
+ "liftoff": "~2.5.0",
+ "nopt": "~4.0.1",
+ "v8flags": "~3.1.1"
+ },
+ "dependencies": {
+ "nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ }
+ }
+ },
+ "grunt-contrib-copy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz",
+ "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=",
+ "requires": {
+ "chalk": "^1.1.1",
+ "file-sync-cmp": "^0.1.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+ }
+ }
+ },
+ "grunt-contrib-sass": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-sass/-/grunt-contrib-sass-1.0.0.tgz",
+ "integrity": "sha1-gGg4JRy8DhqU1k1RXN00z2dNcBs=",
+ "requires": {
+ "async": "^0.9.0",
+ "chalk": "^1.0.0",
+ "cross-spawn": "^0.2.3",
+ "dargs": "^4.0.0",
+ "which": "^1.0.5"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ },
+ "async": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
+ "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+ }
+ }
+ },
+ "grunt-contrib-uglify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-4.0.1.tgz",
+ "integrity": "sha512-dwf8/+4uW1+7pH72WButOEnzErPGmtUvc8p08B0eQS/6ON0WdeQu0+WFeafaPTbbY1GqtS25lsHWaDeiTQNWPg==",
+ "requires": {
+ "chalk": "^2.4.1",
+ "maxmin": "^2.1.0",
+ "uglify-js": "^3.5.0",
+ "uri-path": "^1.0.0"
+ }
+ },
+ "grunt-contrib-watch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz",
+ "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==",
+ "requires": {
+ "async": "^2.6.0",
+ "gaze": "^1.1.0",
+ "lodash": "^4.17.10",
+ "tiny-lr": "^1.1.1"
+ },
+ "dependencies": {
+ "async": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+ "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
+ "requires": {
+ "lodash": "^4.17.14"
+ }
+ }
+ }
+ },
+ "grunt-known-options": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz",
+ "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ=="
+ },
+ "grunt-legacy-log": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz",
+ "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==",
+ "requires": {
+ "colors": "~1.1.2",
+ "grunt-legacy-log-utils": "~2.0.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.5"
+ }
+ },
+ "grunt-legacy-log-utils": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz",
+ "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==",
+ "requires": {
+ "chalk": "~2.4.1",
+ "lodash": "~4.17.10"
+ }
+ },
+ "grunt-legacy-util": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz",
+ "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==",
+ "requires": {
+ "async": "~1.5.2",
+ "exit": "~0.1.1",
+ "getobject": "~0.1.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.10",
+ "underscore.string": "~3.3.4",
+ "which": "~1.3.0"
+ }
+ },
+ "grunt-postcss": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.9.0.tgz",
+ "integrity": "sha512-lglLcVaoOIqH0sFv7RqwUKkEFGQwnlqyAKbatxZderwZGV1nDyKHN7gZS9LUiTx1t5GOvRBx0BEalHMyVwFAIA==",
+ "requires": {
+ "chalk": "^2.1.0",
+ "diff": "^3.0.0",
+ "postcss": "^6.0.11"
+ }
+ },
+ "grunt-string-replace": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/grunt-string-replace/-/grunt-string-replace-1.3.1.tgz",
+ "integrity": "sha1-YzoDvHhIKg4OH5339kWBH8H7sWI=",
+ "requires": {
+ "async": "^2.0.0",
+ "chalk": "^1.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ },
+ "async": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+ "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
+ "requires": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+ }
+ }
+ },
+ "grunt-ts": {
+ "version": "6.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-6.0.0-beta.22.tgz",
+ "integrity": "sha512-g9e+ZImQ7W38dfpwhp0+GUltXWidy3YGPfIA/IyGL5HMv6wmVmMMoSgscI5swhs2HSPf8yAvXAAJbwrouijoRg==",
+ "requires": {
+ "chokidar": "^2.0.4",
+ "csproj2ts": "^1.1.0",
+ "detect-indent": "^4.0.0",
+ "detect-newline": "^2.1.0",
+ "es6-promise": "~0.1.1",
+ "jsmin2": "^1.2.1",
+ "lodash": "~4.17.10",
+ "ncp": "0.5.1",
+ "rimraf": "2.2.6",
+ "semver": "^5.3.0",
+ "strip-bom": "^2.0.0"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz",
+ "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w="
+ }
+ }
+ },
+ "gzip-size": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz",
+ "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=",
+ "requires": {
+ "duplexer": "^0.1.1"
+ }
+ },
+ "handlebars": {
+ "version": "4.7.6",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz",
+ "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==",
+ "requires": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.0",
+ "source-map": "^0.6.1",
+ "uglify-js": "^3.1.4",
+ "wordwrap": "^1.0.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "hark": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/hark/-/hark-1.2.3.tgz",
+ "integrity": "sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg==",
+ "dev": true,
+ "requires": {
+ "wildemitter": "^1.2.0"
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "has-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+ "requires": {
+ "get-value": "^2.0.6",
+ "has-values": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "has-values": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+ "requires": {
+ "is-number": "^3.0.0",
+ "kind-of": "^4.0.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "hash-base": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+ "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "highlight.js": {
+ "version": "9.18.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz",
+ "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg=="
+ },
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "requires": {
+ "parse-passwd": "^1.0.0"
+ }
+ },
+ "hooker": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+ "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk="
+ },
+ "hosted-git-info": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
+ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
+ },
+ "htmlescape": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
+ "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E="
+ },
+ "http-parser-js": {
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz",
+ "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q="
+ },
+ "https-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ieee754": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+ },
+ "indent-string": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "ini": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+ },
+ "inline-source-map": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz",
+ "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=",
+ "requires": {
+ "source-map": "~0.5.3"
+ }
+ },
+ "insert-module-globals": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz",
+ "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "acorn-node": "^1.5.2",
+ "combine-source-map": "^0.8.0",
+ "concat-stream": "^1.6.1",
+ "is-buffer": "^1.1.0",
+ "path-is-absolute": "^1.0.1",
+ "process": "~0.11.0",
+ "through2": "^2.0.0",
+ "undeclared-identifiers": "^1.1.2",
+ "xtend": "^4.0.0"
+ }
+ },
+ "interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
+ },
+ "is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "requires": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+ },
+ "is-binary-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "requires": {
+ "binary-extensions": "^1.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+ },
+ "is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "requires": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="
+ }
+ }
+ },
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+ },
+ "is-finite": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
+ "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="
+ },
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ },
+ "is-number": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "requires": {
+ "is-unc-path": "^1.0.0"
+ }
+ },
+ "is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "requires": {
+ "unc-path-regex": "^0.1.2"
+ }
+ },
+ "is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
+ },
+ "is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+ },
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "jsmin2": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/jsmin2/-/jsmin2-1.2.1.tgz",
+ "integrity": "sha1-iPvi+/dfCpH2YCD9mBzWk/S/5X4="
+ },
+ "json-stable-stringify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz",
+ "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=",
+ "requires": {
+ "jsonify": "~0.0.0"
+ }
+ },
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "jsonify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
+ },
+ "jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
+ },
+ "kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
+ },
+ "labeled-stream-splicer": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz",
+ "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "stream-splicer": "^2.0.0"
+ }
+ },
+ "liftoff": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz",
+ "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=",
+ "requires": {
+ "extend": "^3.0.0",
+ "findup-sync": "^2.0.0",
+ "fined": "^1.0.1",
+ "flagged-respawn": "^1.0.0",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.0",
+ "rechoir": "^0.6.2",
+ "resolve": "^1.1.7"
+ },
+ "dependencies": {
+ "findup-sync": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+ "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ }
+ }
+ }
+ },
+ "livereload-js": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz",
+ "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw=="
+ },
+ "load-json-file": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "strip-bom": "^2.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ },
+ "lodash.memoize": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
+ "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8="
+ },
+ "loud-rejection": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+ "requires": {
+ "currently-unhandled": "^0.4.1",
+ "signal-exit": "^3.0.0"
+ }
+ },
+ "lru-cache": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
+ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI="
+ },
+ "lunr": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz",
+ "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg=="
+ },
+ "make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
+ "map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8="
+ },
+ "map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
+ },
+ "map-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+ "requires": {
+ "object-visit": "^1.0.0"
+ }
+ },
+ "marked": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz",
+ "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw=="
+ },
+ "maxmin": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz",
+ "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=",
+ "requires": {
+ "chalk": "^1.0.0",
+ "figures": "^1.0.1",
+ "gzip-size": "^3.0.0",
+ "pretty-bytes": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+ }
+ }
+ },
+ "md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "meow": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+ "requires": {
+ "camelcase-keys": "^2.0.0",
+ "decamelize": "^1.1.2",
+ "loud-rejection": "^1.0.0",
+ "map-obj": "^1.0.1",
+ "minimist": "^1.1.3",
+ "normalize-package-data": "^2.3.4",
+ "object-assign": "^4.0.1",
+ "read-pkg-up": "^1.0.1",
+ "redent": "^1.0.0",
+ "trim-newlines": "^1.0.0"
+ }
+ },
+ "micromatch": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "braces": "^2.3.1",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "extglob": "^2.0.4",
+ "fragment-cache": "^0.2.1",
+ "kind-of": "^6.0.2",
+ "nanomatch": "^1.2.9",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.2"
+ }
+ },
+ "miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "requires": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ }
+ },
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+ },
+ "mixin-deep": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+ "requires": {
+ "for-in": "^1.0.2",
+ "is-extendable": "^1.0.1"
+ },
+ "dependencies": {
+ "is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "requires": {
+ "is-plain-object": "^2.0.4"
+ }
+ }
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+ },
+ "mkdirp-classic": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz",
+ "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g=="
+ },
+ "module-deps": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.2.tgz",
+ "integrity": "sha512-a9y6yDv5u5I4A+IPHTnqFxcaKr4p50/zxTjcQJaX2ws9tN/W6J6YXnEKhqRyPhl494dkcxx951onSKVezmI+3w==",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "browser-resolve": "^1.7.0",
+ "cached-path-relative": "^1.0.2",
+ "concat-stream": "~1.6.0",
+ "defined": "^1.0.0",
+ "detective": "^5.2.0",
+ "duplexer2": "^0.1.2",
+ "inherits": "^2.0.1",
+ "parents": "^1.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.4.0",
+ "stream-combiner2": "^1.1.1",
+ "subarg": "^1.0.0",
+ "through2": "^2.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "nan": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
+ "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
+ "optional": true
+ },
+ "nanomatch": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+ "requires": {
+ "arr-diff": "^4.0.0",
+ "array-unique": "^0.3.2",
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "fragment-cache": "^0.2.1",
+ "is-windows": "^1.0.2",
+ "kind-of": "^6.0.2",
+ "object.pick": "^1.3.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ }
+ },
+ "ncp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz",
+ "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58="
+ },
+ "neo-async": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
+ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalice": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalice/-/normalice-1.0.1.tgz",
+ "integrity": "sha1-A0NcLuzVYxprygLaOTDsPjRagPc=",
+ "dev": true
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "object-copy": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+ "requires": {
+ "copy-descriptor": "^0.1.0",
+ "define-property": "^0.2.5",
+ "kind-of": "^3.0.3"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "object-visit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+ "requires": {
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=",
+ "requires": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ }
+ },
+ "object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=",
+ "requires": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ }
+ },
+ "object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+ "requires": {
+ "isobject": "^3.0.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
+ "parents": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
+ "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=",
+ "requires": {
+ "path-platform": "~0.11.15"
+ }
+ },
+ "parse-asn1": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz",
+ "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==",
+ "requires": {
+ "asn1.js": "^4.0.0",
+ "browserify-aes": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=",
+ "requires": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ }
+ },
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY="
+ },
+ "pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
+ },
+ "path-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
+ "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ=="
+ },
+ "path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA="
+ },
+ "path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "requires": {
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
+ },
+ "path-platform": {
+ "version": "0.11.15",
+ "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
+ "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I="
+ },
+ "path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=",
+ "requires": {
+ "path-root-regex": "^0.1.0"
+ }
+ },
+ "path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0="
+ },
+ "path-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "pbkdf2": {
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
+ "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+ "requires": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
+ "platform": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz",
+ "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==",
+ "dev": true
+ },
+ "posix-character-classes": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
+ },
+ "postcss": {
+ "version": "6.0.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+ "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "requires": {
+ "chalk": "^2.4.1",
+ "source-map": "^0.6.1",
+ "supports-color": "^5.4.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "pretty-bytes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz",
+ "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
+ },
+ "public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+ },
+ "qs": {
+ "version": "6.9.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
+ "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw=="
+ },
+ "querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+ },
+ "querystring-es3": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "requires": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "raw-body": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz",
+ "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=",
+ "requires": {
+ "bytes": "1",
+ "string_decoder": "0.10"
+ },
+ "dependencies": {
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ }
+ }
+ },
+ "read-only-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
+ "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=",
+ "requires": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "read-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+ "requires": {
+ "load-json-file": "^1.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^1.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+ "requires": {
+ "find-up": "^1.0.0",
+ "read-pkg": "^1.0.0"
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+ "requires": {
+ "resolve": "^1.1.6"
+ }
+ },
+ "redent": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+ "requires": {
+ "indent-string": "^2.1.0",
+ "strip-indent": "^1.0.1"
+ }
+ },
+ "regex-not": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+ "requires": {
+ "extend-shallow": "^3.0.2",
+ "safe-regex": "^1.1.0"
+ }
+ },
+ "remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
+ },
+ "repeat-element": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+ "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g=="
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
+ "resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+ "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
+ "requires": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ }
+ },
+ "resolve-url": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
+ },
+ "ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+ },
+ "safe-json-parse": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz",
+ "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c="
+ },
+ "safe-regex": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "requires": {
+ "ret": "~0.1.10"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
+ "set-value": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "shasum": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz",
+ "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=",
+ "requires": {
+ "json-stable-stringify": "~0.0.0",
+ "sha.js": "~2.4.4"
+ }
+ },
+ "shasum-object": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz",
+ "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==",
+ "requires": {
+ "fast-safe-stringify": "^2.0.7"
+ }
+ },
+ "shell-quote": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz",
+ "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg=="
+ },
+ "shelljs": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz",
+ "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==",
+ "requires": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
+ },
+ "simple-concat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
+ "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
+ },
+ "snapdragon": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+ "requires": {
+ "base": "^0.11.1",
+ "debug": "^2.2.0",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "map-cache": "^0.2.2",
+ "source-map": "^0.5.6",
+ "source-map-resolve": "^0.5.0",
+ "use": "^3.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "snapdragon-node": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+ "requires": {
+ "define-property": "^1.0.0",
+ "isobject": "^3.0.0",
+ "snapdragon-util": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ }
+ }
+ },
+ "snapdragon-util": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+ "requires": {
+ "kind-of": "^3.2.0"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+ },
+ "source-map-resolve": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+ "requires": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ },
+ "source-map-support": {
+ "version": "0.5.18",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.18.tgz",
+ "integrity": "sha512-9luZr/BZ2QeU6tO2uG8N2aZpVSli4TSAOAqFOyTO51AJcD9P99c0K1h6dD6r6qo5dyT44BR5exweOaLLeldTkQ==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "source-map-url": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM="
+ },
+ "spdx-correct": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+ "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+ "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
+ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q=="
+ },
+ "split-string": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "requires": {
+ "extend-shallow": "^3.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
+ },
+ "static-extend": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+ "requires": {
+ "define-property": "^0.2.5",
+ "object-copy": "^0.1.0"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ }
+ }
+ },
+ "stream-browserify": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
+ "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
+ "requires": {
+ "inherits": "~2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "stream-combiner2": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
+ "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=",
+ "requires": {
+ "duplexer2": "~0.1.0",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "stream-http": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.0.tgz",
+ "integrity": "sha512-cuB6RgO7BqC4FBYzmnvhob5Do3wIdIsXAgGycHJnW+981gHqoYcYz9lqjJrk8WXRddbwPuqPYRl+bag6mYv4lw==",
+ "requires": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^3.0.6",
+ "xtend": "^4.0.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ }
+ }
+ },
+ "stream-splicer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz",
+ "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "string-template": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
+ "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0="
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "requires": {
+ "is-utf8": "^0.2.0"
+ }
+ },
+ "strip-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+ "requires": {
+ "get-stdin": "^4.0.1"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ },
+ "subarg": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
+ "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
+ "requires": {
+ "minimist": "^1.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "syntax-error": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz",
+ "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==",
+ "requires": {
+ "acorn-node": "^1.2.0"
+ }
+ },
+ "terser": {
+ "version": "4.6.11",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.11.tgz",
+ "integrity": "sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==",
+ "requires": {
+ "commander": "^2.20.0",
+ "source-map": "~0.6.1",
+ "source-map-support": "~0.5.12"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+ },
+ "through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "requires": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "timers-browserify": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz",
+ "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=",
+ "requires": {
+ "process": "~0.11.0"
+ }
+ },
+ "tiny-lr": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz",
+ "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==",
+ "requires": {
+ "body": "^5.1.0",
+ "debug": "^3.1.0",
+ "faye-websocket": "~0.10.0",
+ "livereload-js": "^2.3.0",
+ "object-assign": "^4.1.0",
+ "qs": "^6.4.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "to-regex": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+ "requires": {
+ "define-property": "^2.0.2",
+ "extend-shallow": "^3.0.2",
+ "regex-not": "^1.0.2",
+ "safe-regex": "^1.1.0"
+ }
+ },
+ "to-regex-range": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+ "requires": {
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1"
+ }
+ },
+ "trim-newlines": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
+ },
+ "tsconfig": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-5.0.3.tgz",
+ "integrity": "sha1-X0J45wGACWeo/Dg/0ZZIh48qbjo=",
+ "requires": {
+ "any-promise": "^1.3.0",
+ "parse-json": "^2.2.0",
+ "strip-bom": "^2.0.0",
+ "strip-json-comments": "^2.0.0"
+ }
+ },
+ "tsify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/tsify/-/tsify-4.0.1.tgz",
+ "integrity": "sha512-ClznEI+pmwY5wmD0J7HCSVERwkD+l71ch3Dqyod2JuQLEsFaiNDI+vPjaGadsuVFVvmzgoI7HghrBtWsSmCDHQ==",
+ "requires": {
+ "convert-source-map": "^1.1.0",
+ "fs.realpath": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "semver": "^5.6.0",
+ "through2": "^2.0.0",
+ "tsconfig": "^5.0.3"
+ }
+ },
+ "tslib": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
+ "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
+ },
+ "tslint": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.1.tgz",
+ "integrity": "sha512-kd6AQ/IgPRpLn6g5TozqzPdGNZ0q0jtXW4//hRcj10qLYBaa3mTUU2y2MCG+RXZm8Zx+KZi0eA+YCrMyNlF4UA==",
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.3",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.10.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ }
+ }
+ },
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "tty-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
+ "typedoc": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.17.4.tgz",
+ "integrity": "sha512-4Lotef1l6lNU5Fulpux809WPlF9CkmcXfv5QFyanrjYlxMFxSdARRdsy8Jv1OU3z0vjR4JsvUQT0YpiPqztcOA==",
+ "requires": {
+ "fs-extra": "^8.1.0",
+ "handlebars": "^4.7.6",
+ "highlight.js": "^9.18.1",
+ "lodash": "^4.17.15",
+ "lunr": "^2.3.8",
+ "marked": "0.8.2",
+ "minimatch": "^3.0.0",
+ "progress": "^2.0.3",
+ "shelljs": "^0.8.3",
+ "typedoc-default-themes": "^0.10.0"
+ }
+ },
+ "typedoc-default-themes": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.10.1.tgz",
+ "integrity": "sha512-SuqAQI0CkwhqSJ2kaVTgl37cWs733uy9UGUqwtcds8pkFK8oRF4rZmCq+FXTGIb9hIUOu40rf5Kojg0Ha6akeg==",
+ "requires": {
+ "lunr": "^2.3.8"
+ }
+ },
+ "typescript": {
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
+ "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w=="
+ },
+ "uglify-js": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.1.tgz",
+ "integrity": "sha512-JUPoL1jHsc9fOjVFHdQIhqEEJsQvfKDjlubcCilu8U26uZ73qOg8VsN8O1jbuei44ZPlwL7kmbAdM4tzaUvqnA==",
+ "requires": {
+ "commander": "~2.20.3"
+ }
+ },
+ "umd": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz",
+ "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow=="
+ },
+ "unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo="
+ },
+ "undeclared-identifiers": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz",
+ "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==",
+ "requires": {
+ "acorn-node": "^1.3.0",
+ "dash-ast": "^1.0.0",
+ "get-assigned-identifiers": "^1.2.0",
+ "simple-concat": "^1.0.0",
+ "xtend": "^4.0.1"
+ }
+ },
+ "underscore.string": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz",
+ "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==",
+ "requires": {
+ "sprintf-js": "^1.0.3",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "union-value": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "requires": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ }
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ },
+ "unset-value": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+ "requires": {
+ "has-value": "^0.3.1",
+ "isobject": "^3.0.0"
+ },
+ "dependencies": {
+ "has-value": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+ "requires": {
+ "get-value": "^2.0.3",
+ "has-values": "^0.1.4",
+ "isobject": "^2.0.0"
+ },
+ "dependencies": {
+ "isobject": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+ "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+ "requires": {
+ "isarray": "1.0.0"
+ }
+ }
+ }
+ },
+ "has-values": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+ "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E="
+ }
+ }
+ },
+ "upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
+ },
+ "uri-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz",
+ "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI="
+ },
+ "urix": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
+ },
+ "url": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+ "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+ "requires": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+ }
+ }
+ },
+ "use": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
+ },
+ "util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "requires": {
+ "inherits": "2.0.3"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ }
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "uuid": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
+ "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
+ "dev": true
+ },
+ "v8flags": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz",
+ "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==",
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
+ },
+ "websocket-driver": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz",
+ "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==",
+ "requires": {
+ "http-parser-js": ">=0.4.0 <0.4.11",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ }
+ },
+ "websocket-extensions": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+ "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg=="
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wildemitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz",
+ "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw==",
+ "dev": true
+ },
+ "wolfy87-eventemitter": {
+ "version": "5.2.9",
+ "resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz",
+ "integrity": "sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==",
+ "dev": true
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ }
+ },
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
+ }
+ }
+ },
+ "opn": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
+ "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==",
+ "dev": true,
+ "requires": {
+ "is-wsl": "^1.1.0"
+ }
+ },
+ "optimize-css-assets-webpack-plugin": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz",
+ "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==",
+ "dev": true,
+ "requires": {
+ "cssnano": "^4.1.10",
+ "last-call-webpack-plugin": "^3.0.0"
+ }
+ },
+ "optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ }
+ },
+ "original": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
+ "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==",
+ "dev": true,
+ "requires": {
+ "url-parse": "^1.4.3"
+ }
+ },
+ "os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+ "dev": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+ "dev": true
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "p-map": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
+ "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
+ "dev": true,
+ "requires": {
+ "aggregate-error": "^3.0.0"
+ }
+ },
+ "p-pipe": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz",
+ "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=",
+ "dev": true
+ },
+ "p-retry": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz",
+ "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==",
+ "dev": true,
+ "requires": {
+ "retry": "^0.12.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true
+ },
+ "parallel-transform": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
+ "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==",
+ "dev": true,
+ "requires": {
+ "cyclist": "^1.0.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.1.5"
+ }
+ },
+ "param-case": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
+ "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
+ "dev": true,
+ "requires": {
+ "no-case": "^2.2.0"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-asn1": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz",
+ "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==",
+ "dev": true,
+ "requires": {
+ "asn1.js": "^4.0.0",
+ "browserify-aes": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ },
+ "dependencies": {
+ "asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "bn.js": {
+ "version": "4.11.9",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+ "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
+ "dev": true
+ }
+ }
+ },
+ "parse-entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
+ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "dev": true,
+ "requires": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ }
+ },
+ "parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ }
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
+ "dev": true
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true
+ },
+ "pascalcase": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+ "dev": true
+ },
+ "path-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
+ "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==",
+ "dev": true
+ },
+ "path-dirname": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
+ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+ "dev": true
+ },
"path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
@@ -13710,9 +18110,9 @@
"dev": true
},
"vuex": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.4.0.tgz",
- "integrity": "sha512-ajtqwEW/QhnrBZQsZxCLHThZZaa+Db45c92Asf46ZDXu6uHXgbfVuBaJ4gzD2r4UX0oMJHstFwd2r2HM4l8umg==",
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
+ "integrity": "sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw==",
"dev": true
},
"watchpack": {
diff --git a/src/package.json b/src/package.json
--- a/src/package.json
+++ b/src/package.json
@@ -16,6 +16,7 @@
"@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "^0.1.10",
+ "anchorme": "^2.1.2",
"axios": "^0.19",
"bootstrap": "^4.5.3",
"cross-env": "^7.0",
@@ -23,6 +24,7 @@
"eslint-plugin-vue": "^6.2.2",
"jquery": "^3.5.1",
"laravel-mix": "^5.0.6",
+ "openvidu-browser": "^2.15.0",
"popper.js": "^1.16.0",
"resolve-url-loader": "^2.3.1",
"sass": "^1.27.0",
@@ -32,6 +34,6 @@
"vue": "^2.6.12",
"vue-router": "^3.4.6",
"vue-template-compiler": "^2.6.12",
- "vuex": "^3.4.0"
+ "vuex": "^3.5.1"
}
}
diff --git a/src/phpstan.neon b/src/phpstan.neon
--- a/src/phpstan.neon
+++ b/src/phpstan.neon
@@ -3,7 +3,7 @@
parameters:
ignoreErrors:
- '#Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable#'
- - '#Access to an undefined property App\\Package::\$pivot#'
+ - '#Access to an undefined property [a-zA-Z\\]+::\$pivot#'
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$id#'
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$created_at#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::toString\(\)#'
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -33,6 +33,13 @@
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
},
+ hasRoute(name) {
+ return this.$router.resolve({ name: name }).resolved.matched.length > 0
+ },
+ hasBeta(name) {
+ const authInfo = store.state.authInfo
+ return authInfo.statusInfo.betaSKUs && authInfo.statusInfo.betaSKUs.indexOf(name) != -1
+ },
isController(wallet_id) {
if (wallet_id && store.state.authInfo) {
let i
@@ -89,11 +96,17 @@
}, timeout * 1000)
},
// Set user state to "not logged in"
- logoutUser() {
+ logoutUser(redirect) {
store.commit('logoutUser')
localStorage.setItem('token', '')
delete axios.defaults.headers.common.Authorization
- this.$router.push({ name: 'login' })
+
+ if (redirect !== false) {
+ if (this.hasRoute('login')) {
+ this.$router.push({ name: 'login' })
+ }
+ }
+
clearTimeout(this.refreshTimeout)
},
// Display "loading" overlay inside of the specified element
@@ -162,7 +175,7 @@
// TODO: This method does not show the download progress in the browser
// but it could be implemented in the UI, axios has 'progress' property
axios.get(url, { responseType: 'blob' })
- .then (response => {
+ .then(response => {
const link = document.createElement('a')
const contentDisposition = response.headers['content-disposition']
let filename = 'unknown'
diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -11,6 +11,7 @@
import {
faCheck,
faCheckCircle,
+ faComments,
faDownload,
faEnvelope,
faGlobe,
@@ -34,6 +35,7 @@
faCheck,
faCheckCircle,
faCheckSquare,
+ faComments,
faCreditCard,
faDownload,
faEnvelope,
diff --git a/src/resources/js/meet.js b/src/resources/js/meet.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/meet.js
@@ -0,0 +1,38 @@
+/**
+ * Application code for the Meet UI
+ */
+
+import routes from './routes-meet.js'
+
+window.routes = routes
+window.isAdmin = false
+
+require('./app')
+
+// Register additional icons
+import { library } from '@fortawesome/fontawesome-svg-core'
+
+import {
+ faAlignLeft,
+ faCompress,
+ faDesktop,
+ faExpand,
+ faMicrophone,
+ faPowerOff,
+ faUser,
+ faVideo,
+ faVolumeMute
+} from '@fortawesome/free-solid-svg-icons'
+
+// Register only these icons we need
+library.add(
+ faAlignLeft,
+ faCompress,
+ faDesktop,
+ faExpand,
+ faMicrophone,
+ faPowerOff,
+ faUser,
+ faVideo,
+ faVolumeMute
+)
diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/meet/app.js
@@ -0,0 +1,963 @@
+import anchorme from 'anchorme'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { OpenVidu } from 'openvidu-browser'
+
+
+function Meet(container)
+{
+ let OV // OpenVidu object to initialize a session
+ let session // Session object where the user will connect
+ let publisher // Publisher object which the user will publish
+ let audioActive = false // True if the audio track of the publisher is active
+ let videoActive = false // True if the video track of the publisher is active
+ let numOfVideos = 0 // Keeps track of the number of videos that are being shown
+ let audioSource = '' // Currently selected microphone
+ let videoSource = '' // Currently selected camera
+ let sessionData // Room session metadata
+
+ let screenOV // OpenVidu object to initialize a screen sharing session
+ let screenSession // Session object where the user will connect for screen sharing
+ let screenPublisher // Publisher object which the user will publish the screen sharing
+
+ let publisherDefaults = {
+ publishAudio: true, // Whether to start publishing with your audio unmuted or not
+ publishVideo: true, // Whether to start publishing with your video enabled or not
+ resolution: '640x480', // The resolution of your video
+ frameRate: 30, // The frame rate of your video
+ mirror: true // Whether to mirror your local video or not
+ }
+
+ let cameras = [] // List of user video devices
+ let microphones = [] // List of user audio devices
+ let connections = {} // Connected users in the session
+
+ let containerWidth
+ let containerHeight
+ let chatCount = 0
+ let volumeElement
+ let setupProps
+
+ OV = new OpenVidu()
+ screenOV = new OpenVidu()
+
+ // If there's anything to do, do it here.
+ //OV.setAdvancedConfiguration(config)
+
+ // Disable all logging except errors
+ // OV.enableProdMode()
+
+ // Disconnect participant when browser's window close
+ window.addEventListener('beforeunload', () => {
+ leaveRoom()
+ })
+
+ window.addEventListener('resize', resize)
+
+ // Public methods
+ this.isScreenSharingSupported = isScreenSharingSupported
+ this.joinRoom = joinRoom
+ this.leaveRoom = leaveRoom
+ this.setup = setup
+ this.setupSetAudioDevice = setupSetAudioDevice
+ this.setupSetVideoDevice = setupSetVideoDevice
+ this.switchAudio = switchAudio
+ this.switchScreen = switchScreen
+ this.switchVideo = switchVideo
+ this.updateSession = updateSession
+
+
+ /**
+ * Join the room session
+ *
+ * @param data Session metadata and event handlers (session, token, shareToken, nickname,
+ * chatElement, menuElement, onDestroy)
+ */
+ function joinRoom(data) {
+ resize();
+ volumeMeterStop()
+
+ data.params = {
+ nickname: data.nickname, // user nickname
+ // avatar: undefined // avatar image
+ }
+
+ sessionData = data
+
+ // Init a session
+ session = OV.initSession()
+
+ // Handle connection creation events
+ session.on('connectionCreated', event => {
+ // Ignore the current user connection
+ if (!event.connection.options) {
+ return
+ }
+
+ // This is the first event executed when a user joins in.
+ // We'll create the video wrapper here, which will be re-used
+ // in 'streamCreated' event handler.
+ // Note: For a user with no cam/mic enabled streamCreated event
+ // is not being dispatched at all
+
+ // TODO: We may consider placing users with no video enabled
+ // in a separate place, so they do not fill the precious
+ // screen estate
+
+ let connectionId = event.connection.connectionId
+ let metadata = JSON.parse(event.connection.data)
+ let wrapper = videoWrapperCreate(container, metadata)
+
+ connections[connectionId] = {
+ element: wrapper
+ }
+
+ updateLayout()
+
+ // Send the current user status to the connecting user
+ // otherwise e.g. nickname might be not up to date
+ signalUserUpdate(event.connection)
+ })
+
+ session.on('connectionDestroyed', event => {
+ let conn = connections[event.connection.connectionId]
+ if (conn) {
+ $(conn.element).remove()
+ numOfVideos--
+ updateLayout()
+ delete connections[event.connection.connectionId]
+ }
+ })
+
+ // On every new Stream received...
+ session.on('streamCreated', event => {
+ let connection = event.stream.connection
+ let connectionId = connection.connectionId
+ let metadata = JSON.parse(connection.data)
+ let wrapper = connections[connectionId].element
+ let props = {
+ // Prepend the video element so it is always before the watermark element
+ insertMode: 'PREPEND'
+ }
+
+ // Subscribe to the Stream to receive it
+ let subscriber = session.subscribe(event.stream, wrapper, props);
+
+ subscriber.on('videoElementCreated', event => {
+ $(event.element).prop({
+ tabindex: -1
+ })
+
+ updateLayout()
+ })
+/*
+ subscriber.on('videoElementDestroyed', event => {
+ })
+*/
+ // Update the wrapper controls/status
+ videoWrapperUpdate(wrapper, event.stream)
+ })
+/*
+ session.on('streamDestroyed', event => {
+ })
+*/
+ // Handle session disconnection events
+ session.on('sessionDisconnected', event => {
+ if (data.onDestroy) {
+ data.onDestroy(event)
+ }
+
+ updateLayout()
+ })
+
+ // Handle signals from all participants
+ session.on('signal', signalEventHandler)
+
+ // Connect with the token
+ session.connect(data.token, data.params)
+ .then(() => {
+ let params = { publisher: true, audioActive, videoActive }
+ let wrapper = videoWrapperCreate(container, Object.assign({}, data.params, params))
+
+ publisher.on('videoElementCreated', event => {
+ $(event.element).prop({
+ muted: true, // Mute local video to avoid feedback
+ tabindex: -1
+ })
+ updateLayout()
+ })
+
+ publisher.createVideoElement(wrapper, 'PREPEND')
+ sessionData.wrapper = wrapper
+
+ // Publish the stream
+ session.publish(publisher)
+ })
+ .catch(error => {
+ console.error('There was an error connecting to the session: ', error.message);
+ })
+
+ // Prepare the chat
+ setupChat()
+ }
+
+ /**
+ * Leave the room (disconnect)
+ */
+ function leaveRoom() {
+ if (publisher) {
+ volumeMeterStop()
+
+ // FIXME: We have to unpublish streams only if there's no session yet
+ if (!session && audioActive) {
+ publisher.publishAudio(false)
+ }
+ if (!session && videoActive) {
+ publisher.publishVideo(false)
+ }
+
+ publisher = null
+ }
+
+ if (session) {
+ session.disconnect();
+ session = null
+ }
+
+ if (screenSession) {
+ screenSession.disconnect();
+ screenSession = null
+ }
+ }
+
+ /**
+ * Sets the audio and video devices for the session.
+ * This will ask user for permission to access media devices.
+ *
+ * @param props Setup properties (videoElement, volumeElement, onSuccess, onError)
+ */
+ function setup(props) {
+ setupProps = props
+
+ publisher = OV.initPublisher(undefined, publisherDefaults)
+
+ publisher.once('accessDenied', error => {
+ props.onError(error)
+ })
+
+ publisher.once('accessAllowed', async () => {
+ let mediaStream = publisher.stream.getMediaStream()
+ let videoStream = mediaStream.getVideoTracks()[0]
+ let audioStream = mediaStream.getAudioTracks()[0]
+
+ audioActive = !!audioStream
+ videoActive = !!videoStream
+ volumeElement = props.volumeElement
+
+ publisher.addVideoElement(props.videoElement)
+
+ volumeMeterStart()
+
+ const devices = await OV.getDevices()
+
+ devices.forEach(device => {
+ // device's props: deviceId, kind, label
+ if (device.kind == 'videoinput') {
+ cameras.push(device)
+ if (videoStream && videoStream.label == device.label) {
+ videoSource = device.deviceId
+ }
+ } else if (device.kind == 'audioinput') {
+ microphones.push(device)
+ if (audioStream && audioStream.label == device.label) {
+ audioSource = device.deviceId
+ }
+ }
+ })
+
+ props.onSuccess({
+ microphones,
+ cameras,
+ audioSource,
+ videoSource,
+ audioActive,
+ videoActive
+ })
+ })
+ }
+
+ /**
+ * Change the publisher audio device
+ *
+ * @param deviceId Device identifier string
+ */
+ async function setupSetAudioDevice(deviceId) {
+ if (!deviceId) {
+ publisher.publishAudio(false)
+ volumeMeterStop()
+ audioActive = false
+ } else if (deviceId == audioSource) {
+ publisher.publishAudio(true)
+ volumeMeterStart()
+ audioActive = true
+ } else {
+ const mediaStream = publisher.stream.mediaStream
+ const oldTrack = mediaStream.getAudioTracks()[0]
+
+ let properties = Object.assign({}, publisherDefaults, {
+ publishAudio: true,
+ publishVideo: videoActive,
+ audioSource: deviceId,
+ videoSource: videoSource
+ })
+
+ volumeMeterStop()
+
+ // Note: We're not using publisher.replaceTrack() as it wasn't working for me
+
+ // Stop and remove the old track
+ if (oldTrack) {
+ oldTrack.stop()
+ mediaStream.removeTrack(oldTrack)
+ }
+
+ // TODO: Handle errors
+
+ await OV.getUserMedia(properties)
+ .then(async (newMediaStream) => {
+ publisher.stream.mediaStream = newMediaStream
+ volumeMeterStart()
+ audioActive = true
+ audioSource = deviceId
+ })
+ }
+
+ return audioActive
+ }
+
+ /**
+ * Change the publisher video device
+ *
+ * @param deviceId Device identifier string
+ */
+ async function setupSetVideoDevice(deviceId) {
+ if (!deviceId) {
+ publisher.publishVideo(false)
+ videoActive = false
+ } else if (deviceId == videoSource) {
+ publisher.publishVideo(true)
+ videoActive = true
+ } else {
+ const mediaStream = publisher.stream.mediaStream
+ const oldTrack = mediaStream.getAudioTracks()[0]
+
+ let properties = Object.assign({}, publisherDefaults, {
+ publishAudio: audioActive,
+ publishVideo: true,
+ audioSource: audioSource,
+ videoSource: deviceId
+ })
+
+ volumeMeterStop()
+
+ // Stop and remove the old track
+ if (oldTrack) {
+ oldTrack.stop()
+ mediaStream.removeTrack(oldTrack)
+ }
+
+ // TODO: Handle errors
+
+ await OV.getUserMedia(properties)
+ .then(async (newMediaStream) => {
+ publisher.stream.mediaStream = newMediaStream
+ volumeMeterStart()
+ videoActive = true
+ videoSource = deviceId
+ })
+ }
+
+ return videoActive
+ }
+
+ /**
+ * Setup the chat UI
+ */
+ function setupChat() {
+ // The UI elements are created in the vue template
+ // Here we add a logic for how they work
+
+ const textarea = $(sessionData.chatElement).find('textarea')
+ const button = $(sessionData.menuElement).find('.link-chat')
+
+ textarea.on('keydown', e => {
+ if (e.keyCode == 13 && !e.shiftKey) {
+ if (textarea.val().length) {
+ signalChat(textarea.val())
+ textarea.val('')
+ }
+
+ return false
+ }
+ })
+
+ // Add an element for the count of unread messages on the chat button
+ button.append('<span class="badge badge-dark blinker">')
+ .on('click', () => {
+ button.find('.badge').text('')
+ chatCount = 0
+ })
+ }
+
+ /**
+ * Signal events handler
+ */
+ function signalEventHandler(signal) {
+ let conn, data
+ let connId = signal.from.connectionId
+
+ switch (signal.type) {
+ case 'signal:userChanged':
+ if (conn = connections[connId]) {
+ data = JSON.parse(signal.data)
+
+ videoWrapperUpdate(conn.element, data)
+ nicknameUpdate(data.nickname, connId)
+ }
+ break
+
+ case 'signal:chat':
+ data = JSON.parse(signal.data)
+ data.id = connId
+ pushChatMessage(data)
+ break
+ }
+ }
+
+ /**
+ * Send the chat message to other participants
+ *
+ * @param message Message string
+ */
+ function signalChat(message) {
+ let data = {
+ nickname: sessionData.params.nickname,
+ message
+ }
+
+ session.signal({
+ data: JSON.stringify(data),
+ type: 'chat'
+ })
+ }
+
+ /**
+ * Add a message to the chat
+ *
+ * @param data Object with a message, nickname, id (of the connection, empty for self)
+ */
+ function pushChatMessage(data) {
+ let message = $('<span>').text(data.message).text() // make the message secure
+
+ // Format the message, convert emails and urls to links
+ message = anchorme({
+ input: message,
+ options: {
+ attributes: {
+ target: "_blank"
+ },
+ // any link above 20 characters will be truncated
+ // to 20 characters and ellipses at the end
+ truncate: 20,
+ // characters will be taken out of the middle
+ middleTruncation: true
+ }
+ // TODO: anchorme is extensible, we could support
+ // github/phabricator's markup e.g. backticks for code samples
+ })
+
+ message = message.replace(/\r?\n/, '<br>')
+
+ // Display the message
+ let isSelf = data.id == publisher.stream.connection.connectionId
+ let chat = $(sessionData.chatElement).find('.chat')
+ let box = chat.find('.message').last()
+
+ message = $('<div>').html(message)
+
+ message.find('a').attr('rel', 'noreferrer')
+
+ if (box.length && box.data('id') == data.id) {
+ // A message from the same user as the last message, no new box needed
+ message.appendTo(box)
+ } else {
+ box = $('<div class="message">').data('id', data.id)
+ .append($('<div class="nickname">').text(data.nickname || ''))
+ .append(message)
+ .appendTo(chat)
+
+ if (isSelf) {
+ box.addClass('self')
+ }
+ }
+
+ // Count unread messages
+ if (!$(sessionData.chatElement).is('.open')) {
+ if (!isSelf) {
+ chatCount++
+ }
+ } else {
+ chatCount = 0
+ }
+
+ $(sessionData.menuElement).find('.link-chat .badge').text(chatCount ? chatCount : '')
+ }
+
+ /**
+ * Send the user properties update signal to other participants
+ *
+ * @param connection Optional connection to which the signal will be sent
+ * If not specified the signal is sent to all participants
+ */
+ function signalUserUpdate(connection) {
+ let data = {
+ audioActive,
+ videoActive,
+ nickname: sessionData.params.nickname
+ }
+
+ // Note: StreamPropertyChangedEvent might be more standard way
+ // to propagate the audio/video state change to other users.
+ // It looks there's no other way to propagate nickname changes.
+ session.signal({
+ data: JSON.stringify(data),
+ type: 'userChanged',
+ to: connection ? [connection] : undefined
+ })
+
+ // The same nickname for screen sharing session
+ if (screenSession) {
+ data.audioActive = false
+ data.videoActive = true
+ screenSession.signal({
+ data: JSON.stringify(data),
+ type: 'userChanged',
+ to: connection ? [connection] : undefined
+ })
+ }
+ }
+
+ /**
+ * Mute/Unmute audio for current session publisher
+ */
+ function switchAudio() {
+ // TODO: If user has no devices or denied access to them in the setup,
+ // the button will just not work. Find a way to make it working
+ // after user unlocks his devices. For now he has to refresh
+ // the page and join the room again.
+ if (microphones.length) {
+ try {
+ publisher.publishAudio(!audioActive)
+ audioActive = !audioActive
+ videoWrapperUpdate(sessionData.wrapper, { audioActive })
+ signalUserUpdate()
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ return audioActive
+ }
+
+ /**
+ * Mute/Unmute video for current session publisher
+ */
+ function switchVideo() {
+ // TODO: If user has no devices or denied access to them in the setup,
+ // the button will just not work. Find a way to make it working
+ // after user unlocks his devices. For now he has to refresh
+ // the page and join the room again.
+ if (cameras.length) {
+ try {
+ publisher.publishVideo(!videoActive)
+ videoActive = !videoActive
+ videoWrapperUpdate(sessionData.wrapper, { videoActive })
+ signalUserUpdate()
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ return videoActive
+ }
+
+ /**
+ * Switch on/off screen sharing
+ */
+ function switchScreen(callback) {
+ if (screenPublisher) {
+ screenSession.disconnect()
+ screenSession = null
+ screenPublisher = null
+
+ if (callback) {
+ // Note: Disconnecting invalidates the token. The callback should request
+ // a new token for the next screen sharing session.
+ callback(false)
+ }
+
+ return
+ }
+
+ screenConnect(callback)
+ }
+
+ /**
+ * Detect if screen sharing is supported by the browser
+ */
+ function isScreenSharingSupported() {
+ return !!OV.checkScreenSharingCapabilities();
+ }
+
+ /**
+ * Update nickname in chat
+ *
+ * @param nickname Nickname
+ * @param connectionId Connection identifier of the user
+ */
+ function nicknameUpdate(nickname, connectionId) {
+ if (connectionId) {
+ $(sessionData.chatElement).find('.chat').find('.message').each(function() {
+ let elem = $(this)
+ if (elem.data('id') == connectionId) {
+ elem.find('.nickname').text(nickname || '')
+ }
+ })
+ }
+ }
+
+ /**
+ * Create a <video> element wrapper with controls
+ *
+ * @param container The parent element
+ * @param params Connection metadata/params
+ */
+ function videoWrapperCreate(container, params) {
+ // Create the element
+ let wrapper = $('<div class="meet-video">').html(
+ `${svgIcon("user", 'fas', 'watermark')}
+ <div class="nickname" title="Nickname">
+ <span></span>
+ <button type="button" class="btn btn-link">${svgIcon('user')}</button>
+ </div>
+ <div class="controls">
+ <button type="button" class="btn btn-link link-audio hidden" title="Mute audio">${svgIcon('volume-mute')}</button>
+ <button type="button" class="btn btn-link link-fullscreen closed hidden" title="Full screen">${svgIcon('expand')}</button>
+ <button type="button" class="btn btn-link link-fullscreen open hidden" title="Full screen">${svgIcon('compress')}</button>
+ </div>
+ <div class="status">
+ <span class="bg-danger status-audio hidden">${svgIcon('microphone')}</span>
+ <span class="bg-danger status-video hidden">${svgIcon('video')}</span>
+ </div>`
+ )
+
+ if (params.publisher) {
+ // Add events for nickname change
+ let nickname = wrapper.addClass('publisher').find('.nickname')
+ let editable = nickname.find('span').get(0)
+ let editableEnable = () => {
+ editable.contentEditable = true
+ editable.focus()
+ }
+ let editableUpdate = () => {
+ editable.contentEditable = false
+ sessionData.params.nickname = editable.innerText
+ signalUserUpdate()
+ nicknameUpdate(editable.innerText, session.connection.connectionId)
+ }
+
+ nickname.on('click', editableEnable)
+
+ $(editable).on('blur', editableUpdate)
+ .on('click', editableEnable)
+ .on('keydown', e => {
+ // Enter or Esc
+ if (e.keyCode == 13 || e.keyCode == 27) {
+ editableUpdate()
+ return false
+ }
+ })
+ } else {
+ wrapper.find('.nickname > svg').addClass('hidden')
+
+ wrapper.find('.link-audio').removeClass('hidden')
+ .on('click', e => {
+ let video = wrapper.find('video')[0]
+ video.muted = !video.muted
+ wrapper.find('.link-audio')[video.muted ? 'addClass' : 'removeClass']('text-danger')
+ })
+ }
+
+ videoWrapperUpdate(wrapper, params)
+
+ // Fullscreen control
+ if (document.fullscreenEnabled) {
+ wrapper.find('.link-fullscreen.closed').removeClass('hidden')
+ .on('click', () => {
+ wrapper.get(0).requestFullscreen()
+ })
+
+ wrapper.find('.link-fullscreen.open')
+ .on('click', () => {
+ document.exitFullscreen()
+ })
+
+ wrapper.on('fullscreenchange', () => {
+ // const enabled = document.fullscreenElement
+ wrapper.find('.link-fullscreen.closed').toggleClass('hidden')
+ wrapper.find('.link-fullscreen.open').toggleClass('hidden')
+ wrapper.toggleClass('fullscreen')
+ })
+ }
+
+ numOfVideos++
+
+ return wrapper[params.publisher ? 'prependTo' : 'appendTo'](container).get(0)
+ }
+
+ /**
+ * Update the <video> wrapper controls
+ *
+ * @param wrapper The wrapper element
+ * @param params Connection metadata/params
+ */
+ function videoWrapperUpdate(wrapper, params) {
+ if ('audioActive' in params) {
+ $(wrapper).find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden')
+ }
+
+ if ('videoActive' in params) {
+ $(wrapper).find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden')
+ }
+
+ if ('nickname' in params) {
+ $(wrapper).find('.nickname > span').text(params.nickname)
+ }
+ }
+
+ /**
+ * Window onresize event handler (updates room layout)
+ */
+ function resize() {
+ containerWidth = container.offsetWidth
+ containerHeight = container.offsetHeight
+ updateLayout()
+ $(container).parent()[window.screen.width <= 768 ? 'addClass' : 'removeClass']('mobile')
+ }
+
+ /**
+ * Update the room "matrix" layout
+ */
+ function updateLayout() {
+ if (!numOfVideos) {
+ return
+ }
+
+ let css, rows, cols, height
+
+ const factor = containerWidth / containerHeight
+
+ if (factor >= 16/9) {
+ if (numOfVideos <= 3) {
+ rows = 1
+ } else if (numOfVideos <= 8) {
+ rows = 2
+ } else if (numOfVideos <= 15) {
+ rows = 3
+ } else if (numOfVideos <= 20) {
+ rows = 4
+ } else {
+ rows = 5
+ }
+
+ cols = Math.ceil(numOfVideos / rows)
+ } else {
+ if (numOfVideos == 1) {
+ cols = 1
+ } else if (numOfVideos <= 4) {
+ cols = 2
+ } else if (numOfVideos <= 9) {
+ cols = 3
+ } else if (numOfVideos <= 16) {
+ cols = 4
+ } else if (numOfVideos <= 25) {
+ cols = 5
+ } else {
+ cols = 6
+ }
+
+ rows = Math.ceil(numOfVideos / cols)
+
+ if (rows < cols && containerWidth < containerHeight) {
+ cols = rows
+ rows = Math.ceil(numOfVideos / cols)
+ }
+ }
+
+ // console.log('factor=' + factor, 'num=' + numOfVideos, 'cols = '+cols, 'rows=' + rows);
+
+ height = containerHeight / rows
+ css = {
+ width: (100 / cols) + '%',
+ // Height must be in pixels to make object-fit:cover working
+ height: height + 'px'
+ }
+
+ // Update the matrix
+ $(container).find('.meet-video').css(css)
+ /*
+ .each((idx, elem) => {
+ let video = $(elem).children('video')[0]
+
+ if (video && video.videoWidth && video.videoHeight && video.videoWidth > video.videoHeight) {
+ // Set max-width to keep the original aspect ratio in cases
+ // when there's enough room to display the element
+ let maxWidth = height * video.videoWidth / video.videoHeight
+ $(elem).css('max-width', maxWidth)
+ }
+ })
+ */
+ }
+
+ /**
+ * Initialize screen sharing session/publisher
+ */
+ function screenConnect(callback) {
+ if (!sessionData.shareToken) {
+ return false
+ }
+
+ let gotSession = !!screenSession
+
+ // Init screen sharing session
+ if (!gotSession) {
+ screenSession = screenOV.initSession();
+ }
+
+ let successFunc = function() {
+ screenSession.publish(screenPublisher)
+ if (callback) {
+ callback(true)
+ }
+ }
+
+ let errorFunc = function() {
+ screenPublisher = null
+ if (callback) {
+ callback(false)
+ }
+ }
+
+ // Init the publisher
+ let params = {
+ videoSource: 'screen',
+ publishAudio: false
+ }
+
+ screenPublisher = screenOV.initPublisher(null, params)
+
+ screenPublisher.once('accessAllowed', (event) => {
+ if (gotSession) {
+ successFunc()
+ } else {
+ screenSession.connect(sessionData.shareToken, sessionData.params)
+ .then(() => {
+ successFunc()
+ })
+ .catch(error => {
+ console.error('There was an error connecting to the session:', error.code, error.message);
+ errorFunc()
+ })
+ }
+ })
+
+ screenPublisher.once('accessDenied', () => {
+ console.info('ScreenShare: Access Denied')
+ errorFunc()
+ })
+ }
+
+ /**
+ * Create an svg element (string) for a FontAwesome icon
+ *
+ * @todo Find if there's a "official" way to do this
+ */
+ function svgIcon(name, type, className) {
+ // Note: the library will contain definitions for all icons registered elswhere
+ const icon = library.definitions[type || 'fas'][name]
+
+ let attrs = {
+ 'class': 'svg-inline--fa',
+ 'aria-hidden': true,
+ focusable: false,
+ role: 'img',
+ xmlns: 'http://www.w3.org/2000/svg',
+ viewBox: `0 0 ${icon[0]} ${icon[1]}`
+ }
+
+ if (className) {
+ attrs['class'] += ' ' + className
+ }
+
+ return $(`<svg><path fill="currentColor" d="${icon[4]}"></path></svg>`)
+ .attr(attrs)
+ .get(0).outerHTML
+ }
+
+ /**
+ * A way to update some session data, after you joined the room
+ *
+ * @param data Same input as for joinRoom(), but for now it supports only shareToken
+ */
+ function updateSession(data) {
+ sessionData.shareToken = data.shareToken
+ }
+
+ /**
+ * A handler for volume level change events
+ */
+ function volumeChangeHandler(event) {
+ let value = 100 + Math.min(0, Math.max(-100, event.value.newValue))
+ let color = 'lime'
+ const bar = volumeElement.firstChild
+
+ if (value >= 70) {
+ color = '#ff3300'
+ } else if (value >= 50) {
+ color = '#ff9933'
+ }
+
+ bar.style.height = value + '%'
+ bar.style.background = color
+ }
+
+ /**
+ * Start the volume meter
+ */
+ function volumeMeterStart() {
+ if (publisher && volumeElement) {
+ publisher.on('streamAudioVolumeChange', volumeChangeHandler)
+ }
+ }
+
+ /**
+ * Stop the volume meter
+ */
+ function volumeMeterStop() {
+ if (publisher && volumeElement) {
+ publisher.off('streamAudioVolumeChange')
+ volumeElement.firstChild.style.height = 0
+ }
+ }
+}
+
+export default Meet
diff --git a/src/resources/js/routes-meet.js b/src/resources/js/routes-meet.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/routes-meet.js
@@ -0,0 +1,23 @@
+import PageComponent from '../vue/Page'
+import LogoutComponent from '../vue/Logout'
+import RoomComponent from '../vue/Meet/Room'
+
+const routes = [
+ {
+ component: LogoutComponent,
+ name: 'logout',
+ path: '/logout'
+ },
+ {
+ component: RoomComponent,
+ name: 'room',
+ path: '/meet/:room'
+ },
+ {
+ component: PageComponent,
+ name: '404',
+ path: '*'
+ }
+]
+
+export default routes
diff --git a/src/resources/js/routes-user.js b/src/resources/js/routes-user.js
--- a/src/resources/js/routes-user.js
+++ b/src/resources/js/routes-user.js
@@ -3,6 +3,7 @@
import DomainListComponent from '../vue/Domain/List'
import LoginComponent from '../vue/Login'
import LogoutComponent from '../vue/Logout'
+import MeetComponent from '../vue/Rooms'
import PageComponent from '../vue/Page'
import PasswordResetComponent from '../vue/PasswordReset'
import SignupComponent from '../vue/Signup'
@@ -59,6 +60,12 @@
meta: { requiresAuth: true }
},
{
+ path: '/rooms',
+ name: 'rooms',
+ component: MeetComponent,
+ meta: { requiresAuth: true }
+ },
+ {
path: '/signup/:param?',
alias: '/signup/voucher/:param',
name: 'signup',
diff --git a/src/resources/lang/en/meet.php b/src/resources/lang/en/meet.php
new file mode 100644
--- /dev/null
+++ b/src/resources/lang/en/meet.php
@@ -0,0 +1,22 @@
+<?php
+
+return [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Pagination Language Lines
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are used by the paginator library to build
+ | the simple pagination links. You are free to change them to anything
+ | you want to customize your views to better match your application.
+ |
+ */
+
+ 'room-not-found' => 'The room does not exist.',
+ 'session-not-found' => 'The session does not exist.',
+ 'session-create-error' => 'Failed to create the session.',
+ 'session-join-error' => 'Failed to join the session.',
+ 'session-close-error' => 'Failed to close the session.',
+ 'session-close-success' => 'The session has been closed successfully.',
+];
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -8,6 +8,7 @@
display: flex;
flex-direction: column;
min-height: 100%;
+ overflow: hidden;
& > nav {
flex-shrink: 0;
@@ -187,6 +188,50 @@
}
}
+.status-message {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .app-loader {
+ width: auto;
+ position: initial;
+
+ .spinner-border {
+ color: $body-color;
+ }
+ }
+
+ svg {
+ font-size: 1.5em;
+ }
+
+ :first-child {
+ margin-right: 0.4em;
+ }
+}
+
+.form-separator {
+ position: relative;
+ margin: 1em 0;
+ display: flex;
+ justify-content: center;
+
+ hr {
+ border-color: #999;
+ margin: 0;
+ position: absolute;
+ top: 0.75em;
+ width: 100%;
+ }
+
+ span {
+ background: #fff;
+ padding: 0 1em;
+ z-index: 1;
+ }
+}
+
#status-box {
background-color: lighten($green, 35);
@@ -212,6 +257,16 @@
}
}
+@keyframes blinker {
+ 50% {
+ opacity: 0;
+ }
+}
+
+.blinker {
+ animation: blinker 750ms step-start infinite;
+}
+
#dashboard-nav {
display: flex;
flex-wrap: wrap;
@@ -244,27 +299,6 @@
}
}
-.form-separator {
- position: relative;
- margin: 1em 0;
- display: flex;
- justify-content: center;
-
- hr {
- border-color: #999;
- margin: 0;
- position: absolute;
- top: 0.75em;
- width: 100%;
- }
-
- span {
- background: #fff;
- padding: 0 1em;
- z-index: 1;
- }
-}
-
// Various improvements for mobile
@include media-breakpoint-down(sm) {
.card,
diff --git a/src/resources/themes/bootstrap.scss b/src/resources/themes/bootstrap.scss
--- a/src/resources/themes/bootstrap.scss
+++ b/src/resources/themes/bootstrap.scss
@@ -2,10 +2,8 @@
@import '~bootstrap/scss/bootstrap';
// Bootstrap style fixes
-.btn-link {
- border: 0;
-}
+.btn-link,
.table thead th {
border: 0;
}
@@ -13,3 +11,7 @@
small {
font-size: 0.875em;
}
+
+.hidden {
+ display: none !important;
+}
diff --git a/src/resources/themes/default/_variables.scss b/src/resources/themes/default/_variables.scss
--- a/src/resources/themes/default/_variables.scss
+++ b/src/resources/themes/default/_variables.scss
@@ -12,5 +12,6 @@
// App colors
$menu-bg-color: $light;
+$menu-gray: #575656;
$main-color: $orange;
$warning: $orange;
diff --git a/src/resources/themes/default/app.scss b/src/resources/themes/default/app.scss
--- a/src/resources/themes/default/app.scss
+++ b/src/resources/themes/default/app.scss
@@ -2,6 +2,7 @@
@import '../bootstrap';
@import '../menu';
+@import '../meet';
@import '../toast';
@import '../forms';
@import '../app';
diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss
new file mode 100644
--- /dev/null
+++ b/src/resources/themes/meet.scss
@@ -0,0 +1,251 @@
+.meet-video {
+ position: relative;
+ background: $menu-bg-color;
+ // Use flexbox for centering .watermark
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .watermark {
+ color: darken($menu-bg-color, 20%);
+ width: 50%;
+ height: 50%;
+ }
+
+ video {
+ // To make object-fit:cover working we have to set the height in pixels
+ // on the wrapper element. This is what javascript method will do.
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ background: #000;
+
+ & + .watermark {
+ display: none;
+ }
+ }
+
+ &.fullscreen {
+ video {
+ // We don't want the video to be cut in fullscreen
+ // This will preserve the aspect ratio of the video stream
+ object-fit: contain;
+ }
+ }
+
+ .controls {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ margin: 0.5em;
+ padding: 0 0.05em;
+ line-height: 2em;
+ border-radius: 1em;
+ background: rgba(#000, 0.7);
+
+ button {
+ line-height: 2;
+ border-radius: 50%;
+ padding: 0;
+ width: 2em;
+ }
+ }
+
+ .status {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ margin: 0.5em;
+ line-height: 2em;
+
+ span {
+ display: inline-block;
+ color: #fff;
+ border-radius: 50%;
+ width: 2em;
+ text-align: center;
+ margin-right: 0.25em;
+ }
+ }
+
+ .nickname {
+ position: absolute;
+ top: 0;
+ left: 0;
+ margin: 0.5em;
+ padding: 0 1em;
+ line-height: 2em;
+ border-radius: 1em;
+ max-width: calc(100% - 1em);
+ background: rgba(#fff, 0.8);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ button {
+ display: none;
+ }
+
+ span {
+ outline: none;
+ }
+ }
+
+ &.publisher .nickname {
+ cursor: pointer;
+ background: rgba($main-color, 0.9);
+
+ &:focus-within {
+ box-shadow: $btn-focus-box-shadow;
+ }
+
+ span:empty {
+ display: block;
+ height: 2em;
+
+ &:not(:focus) + button {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 2em;
+ height: 2em;
+ border-radius: 50%;
+ padding: 0;
+ color: $menu-gray;
+ }
+ }
+ }
+}
+
+#meet-component {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+
+ & + .filler {
+ display: none;
+ }
+}
+
+#app.meet {
+ height: 100%;
+
+ #meet-component {
+ overflow: hidden;
+ }
+}
+
+#meet-setup {
+ max-width: 720px;
+}
+
+#meet-auth {
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ flex: 1;
+}
+
+#meet-session-toolbar {
+ display: flex;
+ justify-content: center;
+}
+
+#meet-session-menu {
+ button {
+ font-size: 1.3em;
+ padding: 0 0.25em;
+ margin: 0.5em;
+ position: relative;
+
+ .badge {
+ font-size: 0.5em;
+ position: absolute;
+ right: -0.5em;
+
+ &:empty {
+ display: none;
+ }
+ }
+ }
+}
+
+#meet-session-layout {
+ flex: 1;
+ overflow: hidden;
+}
+
+#meet-session {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ flex: 1;
+ //overflow: hidden;
+}
+
+#meet-chat {
+ width: 0;
+ display: none;
+ flex-direction: column;
+
+ &.open {
+ width: 30%;
+ display: flex !important;
+
+ .mobile & {
+ width: 100%;
+ z-index: 1;
+ background: $body-bg;
+ }
+ }
+
+ .chat {
+ flex: 1;
+ overflow-y: auto;
+ }
+
+ .message {
+ margin: 0 0.5em 0.5em 0.5em;
+ padding: 0.25em 0.5em;
+ border-radius: 1em;
+ background: $menu-bg-color;
+ overflow-wrap: break-word;
+
+ &.self {
+ background: lighten($main-color, 30%);
+ }
+ }
+
+ .nickname {
+ font-size: 80%;
+ color: $secondary;
+ text-align: right;
+ }
+
+ // TODO: mobile mode
+}
+
+#setup-preview {
+ display: flex;
+
+ video {
+ width: 100%;
+ transform: rotateY(180deg);
+ background: #000;
+ }
+
+ .volume {
+ height: 50%;
+ position: absolute;
+ bottom: 1em;
+ right: 2em;
+ width: 0.5em;
+ background: rgba(0, 0, 0, 0.5);
+
+ .bar {
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ }
+ }
+}
diff --git a/src/resources/views/layouts/app.blade.php b/src/resources/views/layouts/app.blade.php
--- a/src/resources/views/layouts/app.blade.php
+++ b/src/resources/views/layouts/app.blade.php
@@ -9,8 +9,8 @@
<title>{{ config('app.name') }} -- @yield('title')</title>
{{-- TODO: PWA disabled for now: @laravelPWA --}}
- <link rel="icon" type="image/x-icon" href="{{ secure_asset('themes/' . $env['app.theme'] . '/images/favicon.ico') }}">
- <link href="{{ secure_asset('themes/' . $env['app.theme'] . '/app.css') }}" rel="stylesheet">
+ <link rel="icon" type="image/x-icon" href="{{ asset('images/favicon.ico') }}">
+ <link href="@theme_asset(app.css)" rel="stylesheet">
</head>
<body>
<div class="outer-container">
diff --git a/src/resources/views/meet.blade.php b/src/resources/views/meet.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/meet.blade.php
@@ -0,0 +1,10 @@
+@extends('layouts.app')
+@section('title', "Meet")
+@section('content')
+<div id="app">
+ <menu-component mode="header"></menu-component>
+ <app-component></app-component>
+ <div class="filler"></div>
+ <menu-component mode="footer" footer="{{ config('app.company.footer') }}"></menu-component>
+</div>
+@endsection
diff --git a/src/resources/vue/App.vue b/src/resources/vue/App.vue
--- a/src/resources/vue/App.vue
+++ b/src/resources/vue/App.vue
@@ -34,6 +34,7 @@
// Release lock on the router-view, otherwise links (e.g. Logout) will not work
// FIXME: This causes dashboard to call /api/auth/info again
this.isLoading = false
+ this.$root.logoutUser(false)
this.$root.errorHandler(error)
})
} else {
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -16,6 +16,10 @@
<svg-icon icon="wallet"></svg-icon><span class="name">Wallet</span>
<span v-if="balance < 0" class="badge badge-danger">{{ $root.price(balance) }}</span>
</router-link>
+ <router-link v-if="$root.hasBeta('meet')" class="card link-chat" :to="{ name: 'rooms' }">
+ <svg-icon icon="comments"></svg-icon><span class="name">Video chat</span>
+ <span class="badge badge-primary">beta</span>
+ </router-link>
<a v-if="webmailURL" class="card link-webmail" :href="webmailURL">
<svg-icon icon="envelope"></svg-icon><span class="name">Webmail</span>
</a>
diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue
--- a/src/resources/vue/Login.vue
+++ b/src/resources/vue/Login.vue
@@ -43,17 +43,20 @@
</div>
</div>
<div class="mt-1">
- <router-link v-if="!$root.isAdmin" :to="{ name: 'password-reset' }" id="forgot-password">Forgot password?</router-link>
+ <router-link v-if="!$root.isAdmin && $root.hasRoute('password-reset')" :to="{ name: 'password-reset' }" id="forgot-password">Forgot password?</router-link>
<a v-if="webmailURL" :href="webmailURL" class="ml-5" id="webmail">Webmail</a>
</div>
</div>
</template>
-
<script>
export default {
+ props: {
+ dashboard: { type: Boolean, default: true }
+ },
data() {
return {
+ app_url: window.config['app.url'],
email: '',
password: '',
secondFactor: '',
@@ -70,7 +73,8 @@
secondfactor: this.secondFactor
}).then(response => {
// login user and redirect to dashboard
- this.$root.loginUser(response.data)
+ this.$root.loginUser(response.data, this.dashboard)
+ this.$emit('success')
})
}
}
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Meet/Room.vue
@@ -0,0 +1,352 @@
+<template>
+ <div id="meet-component">
+ <div id="meet-session-toolbar" class="hidden">
+ <div id="meet-session-menu">
+ <button class="btn btn-link link-audio" @click="switchSound" title="Mute audio">
+ <svg-icon icon="microphone"></svg-icon>
+ </button>
+ <button class="btn btn-link link-video" @click="switchVideo" title="Mute video">
+ <svg-icon icon="video"></svg-icon>
+ </button>
+ <button class="btn btn-link link-screen text-danger" @click="switchScreen" :disabled="!canShareScreen" title="Share screen">
+ <svg-icon icon="desktop"></svg-icon>
+ </button>
+ <button class="btn btn-link link-chat text-danger" @click="switchChat" title="Chat">
+ <svg-icon icon="align-left"></svg-icon>
+ </button>
+ <button class="btn btn-link link-fullscreen closed hidden" @click="switchFullscreen" title="Full screen">
+ <svg-icon icon="expand"></svg-icon>
+ </button>
+ <button class="btn btn-link link-fullscreen open hidden" @click="switchFullscreen" title="Full screen">
+ <svg-icon icon="compress"></svg-icon>
+ </button>
+ <button class="btn btn-link link-logout" @click="logout" title="Leave session">
+ <svg-icon icon="power-off"></svg-icon>
+ </button>
+ </div>
+ </div>
+
+ <div id="meet-setup" class="card container mt-2 mt-md-5 mb-5">
+ <div class="card-body">
+ <div class="card-title">Set up your session</div>
+ <div class="card-text">
+ <form class="setup-form row">
+ <div id="setup-preview" class="col-sm-6 mb-3 mb-sm-0">
+ <video class="rounded"></video>
+ <div class="volume"><div class="bar"></div></div>
+ </div>
+ <div class="col-sm-6">
+ <div class="form-group">
+ <label for="setup-microphone">Microphone</label>
+ <select class="custom-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange">
+ <option value="">None</option>
+ <option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="setup-camera">Camera</label>
+ <select class="custom-select" id="setup-camera" v-model="camera" @change="setupCameraChange">
+ <option value="">None</option>
+ <option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option>
+ </select>
+ </div>
+ <div class="form-group mb-0">
+ <label for="setup-nickname">Nickname</label>
+ <input class="form-control" type="text" id="setup-nickname" v-model="nickname">
+ </div>
+ </div>
+ <div class="text-center mt-4 col-sm-12">
+ <status-message :status="roomState" :status-labels="roomStateLabels" class="mb-3"></status-message>
+ <button v-if="roomState == 'ready' || roomState == 424"
+ type="button"
+ @click="joinSession"
+ class="btn btn-primary pl-5 pr-5"
+ >JOIN</button>
+ <button v-if="roomState == 423"
+ type="button"
+ @click="joinSession"
+ class="btn btn-primary pl-5 pr-5"
+ >I'm the owner</button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+
+ <div id="meet-session-layout" class="d-flex hidden">
+ <div id="meet-session"></div>
+ <div id="meet-chat">
+ <div class="chat"></div>
+ <div class="chat-input m-2">
+ <textarea class="form-control" rows="1"></textarea>
+ </div>
+ </div>
+ </div>
+
+ <logon-form id="meet-auth" class="hidden" :dashboard="false" @success="authSuccess"></logon-form>
+
+ <div id="leave-dialog" class="modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Room closed</h5>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <p>The session has been closed by the room owner.</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-danger modal-action" data-dismiss="modal">Close</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ import Meet from '../../js/meet/app.js'
+ import StatusMessage from '../Widgets/StatusMessage'
+ import LogonForm from '../Login'
+
+ export default {
+ components: {
+ LogonForm,
+ StatusMessage
+ },
+ data() {
+ return {
+ setup: {
+ cameras: [],
+ microphones: [],
+ },
+ canShareScreen: false,
+ camera: '',
+ meet: null,
+ microphone: '',
+ nickname: '',
+ room: null,
+ roomState: 'init',
+ roomStateLabels: {
+ init: 'Checking the room...',
+ 404: 'The room does not exist.',
+ 423: 'The room is closed. Please, wait for the owner to start the session.',
+ 424: 'The room is closed. It will be open for others after you join.',
+ 500: 'Failed to create a session. Server error.'
+ },
+ session: {}
+ }
+ },
+ mounted() {
+ this.room = this.$route.params.room
+
+ // Initialize OpenVidu and do some basic checks
+ this.meet = new Meet($('#meet-session')[0]);
+ this.canShareScreen = this.meet.isScreenSharingSupported()
+
+ // Check the room and init the session
+ this.initSession()
+
+ // Setup the room UI
+ this.setupSession()
+ },
+ beforeDestroy() {
+ clearTimeout(window.roomRequest)
+
+ if (this.meet) {
+ this.meet.leaveRoom()
+ }
+ },
+ methods: {
+ authSuccess() {
+ // The user (owner) authentication succeeded
+ this.roomState = 'init'
+ this.initSession()
+
+ $('#meet-setup').removeClass('hidden')
+ $('#meet-auth').addClass('hidden')
+ },
+ initSession(init) {
+ let params = []
+
+ if (this.canShareScreen) {
+ params.push('screenShare=1')
+ }
+
+ if (init) {
+ params.push('init=1')
+ }
+
+ const url = '/api/v4/openvidu/rooms/' + this.room + '?' + params.join('&')
+
+ axios.get(url, { ignoreErrors: true })
+ .then(response => {
+ // Response data contains: session, token and shareToken
+ this.roomState = 'ready'
+ this.session = response.data
+
+ if (init) {
+ this.joinSession()
+ }
+ })
+ .catch(error => {
+ this.roomState = String(error.response.status)
+
+ // Waiting for the owner to open the room...
+ if (error.response.status == 423) {
+ // Update room state every 10 seconds
+ window.roomRequest = setTimeout(() => { this.initSession() }, 10000)
+ }
+ })
+
+ if (document.fullscreenEnabled) {
+ $('#meet-session-menu').find('.link-fullscreen.closed').removeClass('hidden')
+ }
+ },
+ joinSession() {
+ if (this.roomState == 423) {
+ $('#meet-setup').addClass('hidden')
+ $('#meet-auth').removeClass('hidden')
+ return
+ }
+
+ if (this.roomState == 424) {
+ this.initSession(true)
+ return
+ }
+
+ clearTimeout(window.roomRequest)
+
+ $('#app').addClass('meet')
+ $('#meet-setup').addClass('hidden')
+ $('#meet-session-toolbar,#meet-session-layout').removeClass('hidden')
+
+ this.session.nickname = this.nickname
+ this.session.menuElement = $('#meet-session-menu')[0]
+ this.session.chatElement = $('#meet-chat')[0]
+ this.session.onDestroy = event => {
+ // TODO: Handle nicely other reasons: disconnect, forceDisconnectByUser,
+ // forceDisconnectByServer, networkDisconnect?
+ if (event.reason == 'sessionClosedByServer' && !this.session.owner) {
+ $('#leave-dialog').on('hide.bs.modal', () => {
+ // FIXME: Where exactly the user should land? Currently he'll land
+ // on dashboard (if he's logged in) or login form (if he's not).
+
+ window.location = window.config['app.url']
+ }).modal()
+ }
+ }
+
+ this.meet.joinRoom(this.session)
+ },
+ logout() {
+ if (this.session.owner) {
+ axios.post('/api/v4/openvidu/rooms/' + this.room + '/close')
+ .then(response => {
+ this.meet.leaveRoom()
+ this.meet = null
+ window.location = window.config['app.url']
+ })
+ } else {
+ this.meet.leaveRoom()
+ this.meet = null
+ window.location = window.config['app.url']
+ }
+ },
+ setMenuItem(type, state, disabled) {
+ let button = $('#meet-session-menu').find('.link-' + type)
+
+ button[state ? 'removeClass' : 'addClass']('text-danger')
+
+ if (disabled !== undefined) {
+ button.prop('disabled', disabled)
+ }
+ },
+ setupSession() {
+ this.meet.setup({
+ videoElement: $('#setup-preview video')[0],
+ volumeElement: $('#setup-preview .volume')[0],
+ onSuccess: setup => {
+ this.setup = setup
+ this.microphone = setup.audioSource
+ this.camera = setup.videoSource
+
+ this.setMenuItem('audio', setup.audioActive)
+ this.setMenuItem('video', setup.videoActive)
+ },
+ onError: error => {
+ this.setMenuItem('audio', false, true)
+ this.setMenuItem('video', false, true)
+ }
+ })
+ },
+ setupCameraChange() {
+ this.meet.setupSetVideoDevice(this.camera).then(enabled => {
+ this.setMenuItem('video', enabled)
+ })
+ },
+ setupMicrophoneChange() {
+ this.meet.setupSetAudioDevice(this.microphone).then(enabled => {
+ this.setMenuItem('audio', enabled)
+ })
+ },
+ switchChat() {
+ let chat = $('#meet-chat')
+ let enabled = chat.is('.open')
+
+ this.setMenuItem('chat', !enabled)
+ chat.toggleClass('open')
+
+ if (!enabled) {
+ chat.find('textarea').focus()
+ }
+
+ // Trigger resize, so participant matrix can update its layout
+ window.dispatchEvent(new Event('resize'));
+ },
+ switchFullscreen() {
+ const element = this.$el
+
+ $(element).off('fullscreenchange').on('fullscreenchange', (e) => {
+ let enabled = document.fullscreenElement == element
+ let buttons = $('#meet-session-menu').find('.link-fullscreen')
+
+ buttons.first()[enabled ? 'addClass' : 'removeClass']('hidden')
+ buttons.last()[!enabled ? 'addClass' : 'removeClass']('hidden')
+ })
+
+ if (document.fullscreenElement) {
+ document.exitFullscreen()
+ } else {
+ element.requestFullscreen()
+ }
+ },
+ switchSound() {
+ const enabled = this.meet.switchAudio()
+ this.setMenuItem('audio', enabled)
+ },
+ switchVideo() {
+ const enabled = this.meet.switchVideo()
+ this.setMenuItem('video', enabled)
+ },
+ switchScreen() {
+ this.meet.switchScreen(enabled => {
+ this.setMenuItem('screen', enabled)
+
+ // After one screen sharing session ended request a new token
+ // for the next screen sharing session
+ if (!enabled) {
+ axios.get('/api/v4/openvidu/rooms/' + this.room, { ignoreErrors: true })
+ .then(response => {
+ // Response data contains: session, token and shareToken
+ this.session.shareToken = response.data.token
+ this.meet.updateSession(this.session)
+ })
+ }
+ })
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Rooms.vue b/src/resources/vue/Rooms.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Rooms.vue
@@ -0,0 +1,56 @@
+<template>
+ <div class="container" dusk="rooms-component">
+ <div id="meet-rooms" class="card">
+ <div class="card-body">
+ <div class="card-title">Video chat <small><sup class="badge badge-primary">beta</sup></small></div>
+ <div class="card-text">
+ <p>We are adding a much requested feature: Video Chat.
+ The basics are working, but it is not a polished product yet.
+ </p>
+ <p>You have one personal video chat room that only you know the location of.
+ It is still in beta, so you can not block people from entering once they know the location
+ and you can not throw them out, once they are in.
+ This functionality will come later. For now, keep that in mind when you share the location
+ of your room.
+ </p>
+ <p>You can access your room and invite people by sharing this link:</p>
+ <a v-if="href" :href="href">{{ href }}</a>
+ <p></p>
+ <p>Keep in mind that this is still in beta and might come with some issues.
+ Should you encounter any on your way, let us know by contacting support.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ rooms: [],
+ href: ''
+ }
+ },
+ mounted() {
+ if (!this.$root.hasBeta('meet')) {
+ this.$root.errorPage(403)
+ return
+ }
+
+ this.$root.startLoading()
+
+ axios.get('/api/v4/openvidu/rooms')
+ .then(response => {
+ this.$root.stopLoading()
+
+ this.rooms = response.data.list
+ if (response.data.count) {
+ this.href = window.config['app.url'] + '/meet/' + encodeURI(this.rooms[0].name)
+ }
+ })
+ .catch(this.$root.errorHandler)
+ }
+ }
+</script>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -272,6 +272,10 @@
axios[method](location, this.user)
.then(response => {
+ if (response.data.statusInfo) {
+ this.$store.state.authInfo.statusInfo = response.data.statusInfo
+ }
+
this.$toast.success(response.data.message)
this.$router.push({ name: 'users' })
})
diff --git a/src/resources/vue/Widgets/Menu.vue b/src/resources/vue/Widgets/Menu.vue
--- a/src/resources/vue/Widgets/Menu.vue
+++ b/src/resources/vue/Widgets/Menu.vue
@@ -13,7 +13,7 @@
<div :id="mode + '-menu-navbar'" :class="'navbar' + (mode == 'header' ? ' collapse navbar-collapse' : '')">
<ul class="navbar-nav">
<li class="nav-item" v-if="!loggedIn">
- <router-link v-if="!$root.isAdmin" class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">Signup</router-link>
+ <router-link v-if="!$root.isAdmin && $root.hasRoute('signup')" class="nav-link link-signup" active-class="active" :to="{name: 'signup'}">Signup</router-link>
<a v-else class="nav-link link-signup" :href="appUrl + '/signup'">Signup</a>
</li>
<li class="nav-item" v-for="item in menu()" :key="item.index">
@@ -30,9 +30,12 @@
<li class="nav-item" v-if="loggedIn">
<router-link class="nav-link menulogin link-logout" active-class="active" :to="{name: 'logout'}">Logout</router-link>
</li>
- <li class="nav-item" v-if="!loggedIn">
+ <li class="nav-item" v-if="!loggedIn && route != 'room'">
<router-link class="nav-link menulogin link-login" active-class="active" :to="{name: 'login'}">Login</router-link>
</li>
+ <li class="nav-item" v-if="!loggedIn && route == 'room'">
+ <a class="nav-link menulogin link-login" :href="app_url + '/login'">Login</a>
+ </li>
</ul>
<div v-if="mode == 'footer'" class="footer">
<div id="footer-copyright">@ Apheleia IT AG, 2020</div>
diff --git a/src/resources/vue/Widgets/StatusMessage.vue b/src/resources/vue/Widgets/StatusMessage.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/StatusMessage.vue
@@ -0,0 +1,49 @@
+<template>
+ <div v-if="status != 'ready'" :class="statusClass()">
+ <div v-if="status == 'init'" class="app-loader small">
+ <div class="spinner-border" role="status"></div>
+ </div>
+ <span v-if="status == 'init'">{{ statusLabel() }}</span>
+
+ <svg-icon v-if="Number(status) >= 400 && status in statusLabels" icon="exclamation-circle"></svg-icon>
+ <span v-if="Number(status) >= 400 && status in statusLabels">{{ statusLabel() }}</span>
+ </div>
+</template>
+
+<script>
+ const defaultLabels = {
+ init: 'Loading...',
+ 404: 'Resource not found.'
+ }
+
+ export default {
+ props: {
+ status: { type: String, default: () => 'init' },
+ statusLabels: { type: Object, default: () => defaultLabels }
+ },
+ methods: {
+ statusClass() {
+ let className = 'status-message'
+
+ if (this.status === 'init') {
+ className += ' loading'
+ } else if (Number(this.status) >= 400) {
+ className += ' text-danger'
+ }
+
+ return className
+ },
+ statusLabel() {
+ if (this.status in this.statusLabels) {
+ return this.statusLabels[this.status]
+ }
+
+ if (this.status in defaultLabels) {
+ return defaultLabels[this.status]
+ }
+
+ return ''
+ }
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -80,6 +80,20 @@
Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate');
Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate');
Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete');
+
+ Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
+ Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
+ }
+);
+
+// Note: In Laravel 7.x we could just use withoutMiddleware() instead of a separate group
+Route::group(
+ [
+ 'domain' => \config('app.domain'),
+ 'prefix' => $prefix . 'api/v4'
+ ],
+ function () {
+ Route::get('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom');
}
);
@@ -101,6 +115,7 @@
],
function () {
Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook');
+ Route::post('meet/openvidu', 'API\V4\OpenViduController@webhook');
}
);
diff --git a/src/routes/web.php b/src/routes/web.php
--- a/src/routes/web.php
+++ b/src/routes/web.php
@@ -1,16 +1,5 @@
<?php
-/*
-|--------------------------------------------------------------------------
-| Web Routes
-|--------------------------------------------------------------------------
-|
-| Here is where you can register web routes for your application. These
-| routes are loaded by the RouteServiceProvider within a group which
-| contains the "web" middleware group. Now create something great!
-|
-*/
-
// We can handle every URL with the default action because
// we have client-side router (including 404 error handler).
// This way we don't have to define any "deep link" routes here.
@@ -26,7 +15,8 @@
Route::fallback(
function () {
- return view('root')->with('env', \App\Utils::uiEnv());
+ $env = \App\Utils::uiEnv();
+ return view($env['view'])->with('env', $env);
}
);
}
diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Meet/RoomControlsTest.php
@@ -0,0 +1,341 @@
+<?php
+
+namespace Tests\Browser\Meet;
+
+use App\OpenVidu\Room;
+use Tests\Browser;
+use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\TestCaseDusk;
+
+class RoomControlsTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->clearBetaEntitlements();
+ }
+
+ public function tearDown(): void
+ {
+ $this->clearBetaEntitlements();
+ parent::tearDown();
+ }
+
+ /**
+ * Test fullscreen buttons
+ *
+ * @group openvidu
+ */
+ public function testFullscreen(): void
+ {
+ // TODO: This test does not work in headless mode
+ $this->markTestIncomplete();
+
+ // Make sure there's no session yet
+ $room = Room::where('name', 'john')->first();
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ $this->browse(function (Browser $browser) {
+ // Join the room as an owner (authenticate)
+ $browser->visit(new RoomPage('john'))
+ ->click('@setup-button')
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@setup-form')
+ ->assertVisible('@login-form')
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->waitFor('@setup-form')
+ ->assertMissing('@login-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->click('@setup-button')
+ ->waitFor('@session')
+
+ // Test fullscreen for the whole room
+ ->click('@menu button.link-fullscreen.closed')
+ ->assertVisible('@toolbar')
+ ->assertVisible('@session')
+ ->assertMissing('nav')
+ ->assertMissing('@menu button.link-fullscreen.closed')
+ ->click('@menu button.link-fullscreen.open')
+ ->assertVisible('nav')
+
+ // Test fullscreen for the participant video
+ ->click('@session button.link-fullscreen.closed')
+ ->assertVisible('@session')
+ ->assertMissing('@toolbar')
+ ->assertMissing('nav')
+ ->assertMissing('@session button.link-fullscreen.closed')
+ ->click('@session button.link-fullscreen.open')
+ ->assertVisible('nav')
+ ->assertVisible('@toolbar');
+ });
+ }
+
+ /**
+ * Test nickname and muting audio/video
+ *
+ * @group openvidu
+ */
+ public function testNicknameAndMuting(): void
+ {
+ // Make sure there's no session yet
+ $room = Room::where('name', 'john')->first();
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ $this->browse(function (Browser $owner, Browser $guest) {
+ // Join the room as an owner (authenticate)
+ $owner->visit(new RoomPage('john'))
+ ->click('@setup-button')
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->type('@setup-nickname-input', 'john')
+ ->click('@setup-button')
+ ->waitFor('@session');
+
+ // In another browser act as a guest
+ $guest->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertMissing('@setup-status-message')
+ ->assertSeeIn('@setup-button', "JOIN")
+ // Join the room, disable cam/mic
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->click('@setup-button')
+ ->waitFor('@session');
+
+ // Assert current UI state
+ $owner->assertToolbar([
+ 'audio' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'video' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
+ 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ ])
+ ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertAudioMuted('video', true)
+ ->assertSeeIn('.nickname', 'john')
+ ->assertMissing('.nickname button')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertMissing('.controls button.link-audio')
+ ->assertMissing('.status .status-audio')
+ ->assertMissing('.status .status-video');
+ })
+ ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
+ $browser->assertMissing('video')
+ ->assertMissing('.nickname')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertVisible('.controls button.link-audio')
+ ->assertVisible('.status .status-audio')
+ ->assertVisible('.status .status-video');
+ })
+ ->assertElementsCount('@session div.meet-video', 2);
+
+ // Assert current UI state
+ $guest->assertToolbar([
+ 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
+ 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ ])
+ ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ $browser->assertVisible('video')
+ //->assertAudioMuted('video', true)
+ ->assertVisible('.nickname button')
+ ->assertMissing('.nickname span')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertMissing('.controls button.link-audio')
+ ->assertVisible('.status .status-audio')
+ ->assertVisible('.status .status-video');
+ })
+ ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertSeeIn('.nickname', 'john')
+ ->assertMissing('.nickname button')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertVisible('.controls button.link-audio')
+ ->assertMissing('.status .status-audio')
+ ->assertMissing('.status .status-video');
+ })
+ ->assertElementsCount('@session div.meet-video', 2);
+
+ // Test nickname change propagation
+
+ // Use script() because type() does not work with this contenteditable widget
+ $guest->setNickname('div.meet-video.publisher', 'guest');
+ $owner->waitFor('div.meet-video:not(.publisher) .nickname')
+ ->assertSeeIn('div.meet-video:not(.publisher) .nickname', 'guest');
+
+ // Test muting audio
+ $owner->click('@menu button.link-audio')
+ ->assertToolbarButtonState('audio', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED)
+ ->assertVisible('div.meet-video.publisher .status .status-audio');
+
+ // FIXME: It looks that we can't just check the <video> element state
+ // We might consider using OpenVidu API to make sure
+ $guest->waitFor('div.meet-video:not(.publisher) .status .status-audio');
+
+ // Test unmuting audio
+ $owner->click('@menu button.link-audio')
+ ->assertToolbarButtonState('audio', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
+ ->assertMissing('div.meet-video.publisher .status .status-audio');
+
+ $guest->waitUntilMissing('div.meet-video:not(.publisher) .status .status-audio');
+
+ // Test muting video
+ $owner->click('@menu button.link-video')
+ ->assertToolbarButtonState('video', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED)
+ ->assertVisible('div.meet-video.publisher .status .status-video');
+
+ // FIXME: It looks that we can't just check the <video> element state
+ // We might consider using OpenVidu API to make sure
+ $guest->waitFor('div.meet-video:not(.publisher) .status .status-video');
+
+ // Test unmuting video
+ $owner->click('@menu button.link-video')
+ ->assertToolbarButtonState('video', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
+ ->assertMissing('div.meet-video.publisher .status .status-video');
+
+ $guest->waitUntilMissing('div.meet-video:not(.publisher) .status .status-video');
+
+ // Test muting other user
+ $guest->with('div.meet-video:not(.publisher)', function (Browser $browser) {
+ $browser->click('.controls button.link-audio')
+ ->assertAudioMuted('video', true)
+ ->assertVisible('.controls button.link-audio.text-danger')
+ ->click('.controls button.link-audio')
+ ->assertAudioMuted('video', false)
+ ->assertVisible('.controls button.link-audio:not(.text-danger)');
+ });
+ });
+ }
+
+ /**
+ * Test text chat
+ *
+ * @group openvidu
+ * @depends testNicknameAndMuting
+ */
+ public function testChat(): void
+ {
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ $this->browse(function (Browser $owner, Browser $guest) {
+ // Join the room as an owner
+ $owner->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->type('@setup-nickname-input', 'john')
+ ->click('@setup-button')
+ ->waitFor('@session');
+
+ // In another browser act as a guest
+ $guest->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertMissing('@setup-status-message')
+ ->assertSeeIn('@setup-button', "JOIN")
+ // Join the room, disable cam/mic
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->click('@setup-button')
+ ->waitFor('@session');
+
+ // Test chat elements
+
+ $owner->click('@menu button.link-chat')
+ ->assertToolbarButtonState('chat', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
+ ->assertVisible('@chat')
+ ->assertVisible('@session')
+ ->assertFocused('@chat-input')
+ ->assertElementsCount('@chat-list .message', 0)
+ ->keys('@chat-input', 'test1', '{enter}')
+ ->assertValue('@chat-input', '')
+ ->assertElementsCount('@chat-list .message', 1)
+ ->assertSeeIn('@chat-list .message .nickname', 'john')
+ ->assertSeeIn('@chat-list .message div:last-child', 'test1');
+
+ $guest->waitFor('@menu button.link-chat .badge')
+ ->assertSeeIn('@menu button.link-chat .badge', '1')
+ ->click('@menu button.link-chat')
+ ->assertToolbarButtonState('chat', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
+ ->assertMissing('@menu button.link-chat .badge')
+ ->assertVisible('@chat')
+ ->assertVisible('@session')
+ ->assertElementsCount('@chat-list .message', 1)
+ ->assertSeeIn('@chat-list .message .nickname', 'john')
+ ->assertSeeIn('@chat-list .message div:last-child', 'test1');
+
+ // Test the number of (hidden) incoming messages
+ $guest->click('@menu button.link-chat')
+ ->assertMissing('@chat');
+
+ $owner->keys('@chat-input', 'test2', '{enter}', 'test3', '{enter}')
+ ->assertElementsCount('@chat-list .message', 1)
+ ->assertSeeIn('@chat-list .message .nickname', 'john')
+ ->assertElementsCount('@chat-list .message div', 4)
+ ->assertSeeIn('@chat-list .message div:last-child', 'test3');
+
+ $guest->waitFor('@menu button.link-chat .badge')
+ ->assertSeeIn('@menu button.link-chat .badge', '2')
+ ->click('@menu button.link-chat')
+ ->assertElementsCount('@chat-list .message', 1)
+ ->assertSeeIn('@chat-list .message .nickname', 'john')
+ ->assertSeeIn('@chat-list .message div:last-child', 'test3')
+ ->keys('@chat-input', 'guest1', '{enter}')
+ ->assertElementsCount('@chat-list .message', 2)
+ ->assertMissing('@chat-list .message:last-child .nickname')
+ ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest1');
+
+ $owner->assertElementsCount('@chat-list .message', 2)
+ ->assertMissing('@chat-list .message:last-child .nickname')
+ ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest1');
+
+ // Test nickname change is propagated to chat messages
+
+ $guest->setNickname('div.meet-video.publisher', 'guest')
+ ->keys('@chat-input', 'guest2', '{enter}')
+ ->assertElementsCount('@chat-list .message', 2)
+ ->assertSeeIn('@chat-list .message:last-child .nickname', 'guest')
+ ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest2');
+
+ $owner->assertElementsCount('@chat-list .message', 2)
+ ->assertSeeIn('@chat-list .message:last-child .nickname', 'guest')
+ ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest2');
+
+ // TODO: Test text chat features, e.g. link handling
+ });
+ }
+
+ /**
+ * Test screen sharing
+ *
+ * @group openvidu
+ */
+ public function testShareScreen(): void
+ {
+ // It looks that screen sharing API is not available in headless chrome
+ // Note that other tests already assert that the button is disabled
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Tests\Browser\Meet;
+
+use App\OpenVidu\Room;
+use Tests\Browser;
+use Tests\Browser\Components\Dialog;
+use Tests\Browser\Components\Menu;
+use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\TestCaseDusk;
+
+class RoomSetupTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->clearBetaEntitlements();
+ }
+
+ public function tearDown(): void
+ {
+ $this->clearBetaEntitlements();
+ parent::tearDown();
+ }
+
+ /**
+ * Test non-existing room
+ *
+ * @group openvidu
+ */
+ public function testRoomNonExistingRoom(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new RoomPage('unknown'))
+ ->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
+ });
+ } else {
+ $browser->assertMissing('#footer-menu .navbar-nav');
+ }
+
+ $browser->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->assertVisible('@setup-form')
+ ->assertSeeIn('@setup-status-message', "The room does not exist.")
+ ->assertMissing('@setup-button');
+ });
+ }
+
+ /**
+ * Test the room setup page
+ *
+ * @group openvidu
+ */
+ public function testRoomSetup(): void
+ {
+ // Make sure there's no session yet
+ $room = Room::where('name', 'john')->first();
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new RoomPage('john'))
+ ->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
+ });
+ } else {
+ $browser->assertMissing('#footer-menu .navbar-nav');
+ }
+
+ // Note: I've found out that if I have another Chrome instance running
+ // that uses media, here the media devices will not be available
+
+ // TODO: Test enabling/disabling cam/mic in the setup widget
+
+ $browser->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->assertVisible('@setup-form')
+ ->assertSeeIn('@setup-title', 'Set up your session')
+ ->assertVisible('@setup-video')
+ ->assertSeeIn('@setup-form .form-group:nth-child(1) label', 'Microphone')
+ ->assertVisible('@setup-mic-select')
+ ->assertSeeIn('@setup-form .form-group:nth-child(2) label', 'Camera')
+ ->assertVisible('@setup-cam-select')
+ ->assertSeeIn('@setup-form .form-group:nth-child(3) label', 'Nickname')
+ ->assertValue('@setup-nickname-input', '')
+ ->assertSeeIn(
+ '@setup-status-message',
+ "The room is closed. Please, wait for the owner to start the session."
+ )
+ ->assertSeeIn('@setup-button', "I'm the owner");
+ });
+ }
+
+ /**
+ * Test two users in a room (joining/leaving and some basic functionality)
+ *
+ * @group openvidu
+ * @depends testRoomSetup
+ */
+ public function testTwoUsersInARoom(): void
+ {
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ $this->browse(function (Browser $browser, Browser $guest) {
+ // In one browser window act as a guest
+ $guest->visit(new RoomPage('john'))
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertSeeIn(
+ '@setup-status-message',
+ "The room is closed. Please, wait for the owner to start the session."
+ )
+ ->assertSeeIn('@setup-button', "I'm the owner");
+
+ // In another window join the room as the owner (authenticate)
+ $browser->on(new RoomPage('john'))
+ ->assertSeeIn('@setup-button', "I'm the owner")
+ ->click('@setup-button')
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@setup-form')
+ ->assertVisible('@login-form')
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->waitFor('@setup-form')
+ ->assertMissing('@login-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertSeeIn('@setup-status-message', "The room is closed. It will be open for others after you join.")
+ ->assertSeeIn('@setup-button', "JOIN")
+ ->type('@setup-nickname-input', 'john')
+ // Join the room
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertSeeIn('.nickname', 'john')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertMissing('.controls button.link-audio')
+ ->assertMissing('.status .status-audio')
+ ->assertMissing('.status .status-video');
+ })
+ ->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['explore', 'blog', 'support', 'logout']);
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
+ $browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'logout']);
+ });
+ }
+
+ // After the owner "opened the room" guest should be able to join
+ $guest->waitUntilMissing('@setup-status-message', 10)
+ ->assertSeeIn('@setup-button', "JOIN")
+ // Join the room, disable cam/mic
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertVisible('.nickname button')
+ ->assertMissing('.nickname span')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertMissing('.controls button.link-audio')
+ ->assertVisible('.status .status-audio')
+ ->assertVisible('.status .status-video');
+ })
+ ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertSeeIn('.nickname', 'john')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertVisible('.controls button.link-audio')
+ ->assertMissing('.status .status-audio')
+ ->assertMissing('.status .status-video');
+ })
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
+ });
+
+ if ($guest->isDesktop()) {
+ $guest->within(new Menu('footer'), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
+ });
+ }
+
+ // Check guest's elements in the owner's window
+ $browser->waitFor('@session div.meet-video:nth-child(2)')
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
+ $browser->assertMissing('video')
+ ->assertMissing('.nickname')
+ ->assertVisible('.controls button.link-fullscreen')
+ ->assertVisible('.controls button.link-audio')
+ ->assertVisible('.status .status-audio')
+ ->assertVisible('.status .status-video');
+ });
+
+ // Test leaving the room
+
+ // Guest is leaving
+ $guest->click('@menu button.link-logout')
+ ->waitForLocation('/login');
+
+ // Expect the participant removed from other users windows
+ $browser->waitUntilMissing('@session div.meet-video:nth-child(2)');
+
+ // Join the room as guest again
+ $guest->visit(new RoomPage('john'))
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->assertMissing('@setup-status-message')
+ ->assertSeeIn('@setup-button', "JOIN")
+ // Join the room, disable cam/mic
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->click('@setup-button')
+ ->waitFor('@session');
+
+ // Leave the room as the room owner
+ // TODO: Test leaving the room by closing the browser window,
+ // it should not destroy the session
+ $browser->click('@menu button.link-logout')
+ ->waitForLocation('/dashboard');
+
+ // Expect other participants be informed about the end of the session
+ $guest->with(new Dialog('#leave-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Room closed')
+ ->assertSeeIn('@body', "The session has been closed by the room owner.")
+ ->assertMissing('@button-cancel')
+ ->assertSeeIn('@button-action', 'Close')
+ ->click('@button-action');
+ })
+ ->assertMissing('#leave-dialog')
+ ->waitForLocation('/login');
+ });
+ }
+}
diff --git a/src/tests/Browser/Meet/RoomsTest.php b/src/tests/Browser/Meet/RoomsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Meet/RoomsTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Tests\Browser\Meet;
+
+use App\Sku;
+use Tests\Browser;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\Browser\Pages\UserInfo;
+use Tests\TestCaseDusk;
+
+class RoomsTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->clearBetaEntitlements();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->clearBetaEntitlements();
+ parent::tearDown();
+ }
+
+ /**
+ * Test rooms page (unauthenticated and unauthorized)
+ *
+ * @group openvidu
+ */
+ public function testRoomsUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/rooms')
+ ->on(new Home())
+ // User has no 'meet' entitlement yet, expect redirect to error page
+ ->submitLogon('john@kolab.org', 'simple123', false)
+ ->waitFor('#app > #error-page')
+ ->assertSeeIn('#error-page .code', '403')
+ ->assertSeeIn('#error-page .message', 'Access denied');
+ });
+ }
+
+ /**
+ * Test rooms page
+ *
+ * @group openvidu
+ */
+ public function testRooms(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $href = \config('app.url') . '/meet/john';
+ $john = $this->getTestUser('john@kolab.org');
+ $john->assignSku(Sku::where('title', 'beta')->first());
+
+ // User has no 'meet' entitlement yet
+ $browser->visit('/login')
+ ->on(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', true)
+ ->on(new Dashboard())
+ ->assertMissing('@links a.link-chat');
+
+ // Goto user subscriptions, and enable 'meet' subscription
+ $browser->visit('/user/' . $john->id)
+ ->on(new UserInfo())
+ ->with('@skus', function ($browser) {
+ $browser->click('#sku-input-meet');
+ })
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
+ ->click('.navbar-brand')
+ ->on(new Dashboard())
+ ->assertSeeIn('@links a.link-chat', 'Video chat')
+ // Make sure the element also exists on Dashboard page load
+ ->refresh()
+ ->on(new Dashboard())
+ ->assertSeeIn('@links a.link-chat', 'Video chat');
+
+ // Test Video chat page
+ $browser->click('@links a.link-chat')
+ ->waitFor('#meet-rooms')
+ ->waitFor('.card-text a')
+ ->assertSeeIn('.card-title', 'Video chat')
+ ->assertSeeIn('.card-text a', $href)
+ ->assertAttribute('.card-text a', 'href', $href)
+ ->click('.card-text a')
+ ->on(new RoomPage('john'))
+ // check that entering the room skips the logon form
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->assertVisible('@setup-form')
+ ->assertSeeIn('@setup-status-message', "The room is closed. It will be open for others after you join.")
+ ->assertSeeIn('@setup-button', "JOIN")
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form');
+ });
+ }
+}
diff --git a/src/tests/Browser/Pages/Meet/Room.php b/src/tests/Browser/Pages/Meet/Room.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Meet/Room.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace Tests\Browser\Pages\Meet;
+
+use Laravel\Dusk\Page;
+use PHPUnit\Framework\Assert;
+
+class Room extends Page
+{
+ public const BUTTON_ACTIVE = 1;
+ public const BUTTON_ENABLED = 2;
+ public const BUTTON_INACTIVE = 4;
+ public const BUTTON_DISABLED = 8;
+
+ protected $roomName;
+
+ /**
+ * Object constructor.
+ *
+ * @param string $name Room name
+ */
+ public function __construct($name)
+ {
+ $this->roomName = $name;
+ }
+
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url()
+ {
+ return '/meet/' . $this->roomName;
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->waitForLocation($this->url())
+ ->waitUntilMissing('.app-loader')
+ ->waitUntilMissing('#meet-setup div.status-message.loading');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements()
+ {
+ return [
+ '@app' => '#app',
+
+ '@setup-form' => '#meet-setup form',
+ '@setup-title' => '#meet-setup .card-title',
+ '@setup-mic-select' => '#setup-microphone',
+ '@setup-cam-select' => '#setup-camera',
+ '@setup-nickname-input' => '#setup-nickname',
+ '@setup-preview' => '#setup-preview',
+ '@setup-volume' => '#setup-preview .volume',
+ '@setup-video' => '#setup-preview video',
+ '@setup-status-message' => '#meet-setup div.status-message',
+ '@setup-button' => '#meet-setup form button',
+
+ '@toolbar' => '#meet-session-toolbar',
+
+ '@menu' => '#meet-session-menu',
+
+ '@session' => '#meet-session',
+
+ '@chat' => '#meet-chat',
+ '@chat-input' => '#meet-chat textarea',
+ '@chat-list' => '#meet-chat .chat',
+
+ '@login-form' => '#meet-auth',
+ '@login-email-input' => '#inputEmail',
+ '@login-password-input' => '#inputPassword',
+ '@login-second-factor-input' => '#secondfactor',
+ '@login-button' => '#meet-auth button',
+ ];
+ }
+
+ /**
+ * Assert menu state.
+ *
+ * @param \Tests\Browser $browser The browser object
+ * @param array $menu Menu items/state
+ */
+ public function assertToolbar($browser, array $menu): void
+ {
+ $browser->assertElementsCount('@menu button', count($menu));
+
+ foreach ($menu as $item => $state) {
+ $this->assertToolbarButtonState($browser, $item, $state);
+ }
+ }
+
+ /**
+ * Assert menu button state.
+ *
+ * @param \Tests\Browser $browser The browser object
+ * @param string $button Button name
+ * @param int $state Expected button state (sum of BUTTON_* consts)
+ */
+ public function assertToolbarButtonState($browser, $button, $state): void
+ {
+ $class = '';
+
+ if ($state & self::BUTTON_ACTIVE) {
+ $class .= ':not(.text-danger)';
+ }
+
+ if ($state & self::BUTTON_INACTIVE) {
+ $class .= '.text-danger';
+ }
+
+ if ($state & self::BUTTON_DISABLED) {
+ $class .= '[disabled]';
+ }
+
+ if ($state & self::BUTTON_ENABLED) {
+ $class .= ':not([disabled])';
+ }
+
+ $browser->assertVisible('@menu button.link-' . $button . $class);
+ }
+
+ /**
+ * Assert the <video> element's 'muted' property state
+ *
+ * @param \Tests\Browser $browser The browser object
+ * @param string $selector Video element selector
+ * @param bool $state Expected state
+ */
+ public function assertAudioMuted($browser, $selector, $state): void
+ {
+ $selector = addslashes($browser->resolver->format($selector));
+
+ $result = $browser->script(
+ "var video = document.querySelector('$selector'); return video.muted"
+ );
+
+ Assert::assertSame((bool) $result[0], $state);
+ }
+
+ /**
+ * Set the nickname for the participant
+ *
+ * @param \Tests\Browser $browser The browser object
+ * @param string $selector Participant element selector
+ * @param string $nickname Nickname
+ */
+ public function setNickname($browser, $selector, $nickname): void
+ {
+ // Use script() because type() does not work with this contenteditable widget
+ $selector = $selector . ' .nickname span';
+ $browser->script(
+ "var element = document.querySelector('$selector');"
+ . "element.focus();"
+ . "element.innerText = '$nickname';"
+ . "element.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 }))"
+ );
+ }
+
+ /**
+ * Submit logon form.
+ *
+ * @param \Tests\Browser $browser The browser object
+ * @param string $username User name
+ * @param string $password User password
+ * @param array $config Client-site config
+ */
+ public function submitLogon($browser, $username, $password, $config = []): void
+ {
+ $browser->type('@login-email-input', $username)
+ ->type('@login-password-input', $password);
+
+ if ($username == 'ned@kolab.org') {
+ $code = \App\Auth\SecondFactor::code('ned@kolab.org');
+ $browser->type('@login-second-factor-input', $code);
+ }
+
+ if (!empty($config)) {
+ $browser->script(
+ sprintf('Object.assign(window.config, %s)', \json_encode($config))
+ );
+ }
+
+ $browser->click('@login-button');
+ }
+}
diff --git a/src/tests/Browser/Pages/PaymentMollie.php b/src/tests/Browser/Pages/PaymentMollie.php
--- a/src/tests/Browser/Pages/PaymentMollie.php
+++ b/src/tests/Browser/Pages/PaymentMollie.php
@@ -48,7 +48,7 @@
* Submit payment form.
*
* @param \Laravel\Dusk\Browser $browser The browser object
- * @param string $state Test payment status (paid, open, failed, canceled, expired)
+ * @param string $status Test payment status (paid, open, failed, canceled, expired)
*
* @return void
*/
diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php
--- a/src/tests/Feature/Controller/DomainsTest.php
+++ b/src/tests/Feature/Controller/DomainsTest.php
@@ -2,7 +2,6 @@
namespace Tests\Feature\Controller;
-use App\Http\Controllers\API\DomainsController;
use App\Domain;
use App\Entitlement;
use App\Sku;
diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/OpenViduTest.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Http\Controllers\API\V4\OpenViduController;
+use App\OpenVidu\Room;
+use Tests\TestCase;
+
+class OpenViduTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->clearBetaEntitlements();
+ }
+
+ public function tearDown(): void
+ {
+ $this->clearBetaEntitlements();
+ parent::tearDown();
+ }
+
+ /**
+ * Test listing user rooms
+ *
+ * @group openvidu
+ */
+ public function testIndex(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ Room::where('user_id', $jack->id)->delete();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/openvidu/rooms");
+ $response->assertStatus(401);
+
+ // John has one room
+ $response = $this->actingAs($john)->get("api/v4/openvidu/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame('john', $json['list'][0]['name']);
+
+ // Jack has no room, but it will be auto-created
+ $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertRegExp('/^[0-9A-Z]{8}$/', $json['list'][0]['name']);
+ }
+
+ /**
+ * Test joining the room
+ *
+ * @group openvidu
+ */
+ public function testJoinRoom(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = Room::where('name', 'john')->first();
+ $room->session_id = null;
+ $room->save();
+
+ $this->assignBetaEntitlement($john, 'meet');
+
+ // Unauth access, no session yet
+ $response = $this->get("api/v4/openvidu/rooms/{$room->name}");
+ $response->assertStatus(423);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/non-existing");
+ $response->assertStatus(404);
+
+ // Non-owner, no session yet
+ $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms/{$room->name}");
+ $response->assertStatus(423);
+
+ // Room owner, no session yet
+ $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/{$room->name}");
+ $response->assertStatus(424);
+
+ $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/{$room->name}?init=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $session_id = $room->fresh()->session_id;
+
+ $this->assertSame('PUBLISHER', $json['role']);
+ $this->assertSame($session_id, $json['session']);
+ $this->assertTrue(is_string($session_id) && !empty($session_id));
+ $this->assertTrue(strpos($json['token'], 'wss://') === 0);
+ $this->assertTrue(!array_key_exists('shareToken', $json));
+
+ $john_token = $json['token'];
+
+ // Non-owner, now the session exists
+ $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms/{$room->name}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('PUBLISHER', $json['role']);
+ $this->assertSame($session_id, $json['session']);
+ $this->assertTrue(strpos($json['token'], 'wss://') === 0);
+ $this->assertTrue($json['token'] != $john_token);
+ $this->assertTrue(!array_key_exists('shareToken', $json));
+
+ // TODO: Test accessing an existing room of deleted owner
+ }
+
+ /**
+ * Test joining the room
+ *
+ * @group openvidu
+ * @depends testJoinRoom
+ */
+ public function testJoinRoomGuest(): void
+ {
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ // There's no asy way to logout the user in the same test after
+ // using actingAs(). That's why this is moved to a separate test
+ $room = Room::where('name', 'john')->first();
+
+ // Guest, request with screenShare token
+ $response = $this->get("api/v4/openvidu/rooms/{$room->name}?screenShare=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('PUBLISHER', $json['role']);
+ $this->assertSame($room->session_id, $json['session']);
+ $this->assertTrue(strpos($json['token'], 'wss://') === 0);
+ $this->assertTrue(strpos($json['shareToken'], 'wss://') === 0);
+ $this->assertTrue($json['shareToken'] != $json['token']);
+ }
+
+ /**
+ * Test closing the room (session)
+ *
+ * @group openvidu
+ * @depends testJoinRoom
+ */
+ public function testCloseRoom(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = Room::where('name', 'john')->first();
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/openvidu/rooms/{$room->name}/close", []);
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/close", []);
+ $response->assertStatus(404);
+
+ // Non-owner
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/close", []);
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/close", []);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertNull($room->fresh()->session_id);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("The session has been closed successfully.", $json['message']);
+ $this->assertCount(2, $json);
+
+ // TODO: Test if the session is removed from the OpenVidu server too
+
+ // Test error handling when it's not possible to delete the session on
+ // the OpenVidu server (use fake session_id)
+ $room->session_id = 'aaa';
+ $room->save();
+
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/close", []);
+ $response->assertStatus(500);
+
+ $json = $response->json();
+
+ $this->assertSame('aaa', $room->fresh()->session_id);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("Failed to close the session.", $json['message']);
+ $this->assertCount(2, $json);
+ }
+}
diff --git a/src/tests/Feature/Controller/PackagesTest.php b/src/tests/Feature/Controller/PackagesTest.php
--- a/src/tests/Feature/Controller/PackagesTest.php
+++ b/src/tests/Feature/Controller/PackagesTest.php
@@ -2,7 +2,6 @@
namespace Tests\Feature\Controller;
-use App\Http\Controllers\API\PackagesController;
use App\Package;
use Tests\TestCase;
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -357,6 +357,7 @@
$result = UsersController::statusInfo($user);
$this->assertFalse($result['isReady']);
+ $this->assertSame([], $result['betaSKUs']);
$this->assertCount(3, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
@@ -395,6 +396,7 @@
$result = UsersController::statusInfo($user);
$this->assertFalse($result['isReady']);
+ $this->assertSame([], $result['betaSKUs']);
$this->assertCount(7, $result['process']);
$this->assertSame('user-new', $result['process'][0]['label']);
$this->assertSame(true, $result['process'][0]['state']);
@@ -410,7 +412,27 @@
$this->assertSame(true, $result['process'][5]['state']);
$this->assertSame('domain-confirmed', $result['process'][6]['label']);
$this->assertSame(false, $result['process'][6]['state']);
+
+ // Test betaSKUs property
+ $user->assignSku(Sku::where('title', 'beta')->first());
+
+ $result = UsersController::statusInfo($user);
+
+ $this->assertSame([], $result['betaSKUs']);
+
+ $user->assignSku(Sku::where('title', 'meet')->first());
+
+ $result = UsersController::statusInfo($user);
+
+ $this->assertSame(['meet'], $result['betaSKUs']);
+
+ $user->assignSku(Sku::where('title', 'meet')->first());
+
+ $result = UsersController::statusInfo($user);
+
+ $this->assertSame(['meet'], $result['betaSKUs']);
}
+
/**
* Test user creation (POST /api/v4/users)
*/
@@ -579,7 +601,8 @@
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
- $this->assertCount(2, $json);
+ $this->assertTrue(!empty($json['statusInfo']));
+ $this->assertCount(3, $json);
// Test some invalid data
$post = ['password' => '12345678', 'currency' => 'invalid'];
@@ -615,7 +638,8 @@
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
- $this->assertCount(2, $json);
+ $this->assertTrue(!empty($json['statusInfo']));
+ $this->assertCount(3, $json);
$this->assertTrue($userA->password != $userA->fresh()->password);
unset($post['password'], $post['password_confirmation'], $post['aliases']);
foreach ($post as $key => $value) {
@@ -646,7 +670,8 @@
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
- $this->assertCount(2, $json);
+ $this->assertTrue(!empty($json['statusInfo']));
+ $this->assertCount(3, $json);
unset($post['aliases']);
foreach ($post as $key => $value) {
$this->assertNull($userA->getSetting($key));
@@ -678,9 +703,13 @@
$this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]);
// Test authorized update of other user
- $response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}", []);
+ $response = $this->actingAs($ned)->put("/api/v4/users/{$jack->id}", []);
$response->assertStatus(200);
+ $json = $response->json();
+
+ $this->assertTrue(empty($json['statusInfo']));
+
// TODO: Test error on aliases with invalid/non-existing/other-user's domain
// Create entitlements and additional user for following tests
@@ -722,6 +751,8 @@
$response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post);
$response->assertStatus(200);
+ $json = $response->json();
+
$storage_cost = $user->entitlements()
->where('sku_id', $sku_storage->id)
->orderBy('cost')
@@ -733,6 +764,7 @@
);
$this->assertSame([0, 0, 25], $storage_cost);
+ $this->assertTrue(empty($json['statusInfo']));
}
/**
@@ -916,6 +948,7 @@
$this->assertTrue($result['statusInfo']['enableDomains']);
$this->assertTrue($result['statusInfo']['enableWallets']);
$this->assertTrue($result['statusInfo']['enableUsers']);
+ $this->assertSame([], $result['statusInfo']['betaSKUs']);
// Ned is John's wallet controller
$ned = $this->getTestUser('ned@kolab.org');
@@ -937,6 +970,7 @@
$this->assertTrue($result['statusInfo']['enableDomains']);
$this->assertTrue($result['statusInfo']['enableWallets']);
$this->assertTrue($result['statusInfo']['enableUsers']);
+ $this->assertSame([], $result['statusInfo']['betaSKUs']);
// Test discount in a response
$discount = Discount::where('code', 'TEST')->first();
@@ -965,6 +999,7 @@
$this->assertFalse($result['statusInfo']['enableDomains']);
$this->assertFalse($result['statusInfo']['enableWallets']);
$this->assertFalse($result['statusInfo']['enableUsers']);
+ $this->assertSame([], $result['statusInfo']['betaSKUs']);
}
/**
diff --git a/src/tests/TestCaseDusk.php b/src/tests/TestCaseDusk.php
--- a/src/tests/TestCaseDusk.php
+++ b/src/tests/TestCaseDusk.php
@@ -33,6 +33,9 @@
'--lang=en_US',
'--disable-gpu',
'--headless',
+ '--use-fake-ui-for-media-stream',
+ '--ignore-certificate-errors',
+ '--incognito',
]);
// For file download handling
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -12,6 +12,23 @@
trait TestCaseTrait
{
+ /**
+ * Assign beta entitlement to a user.
+ * It will add both requested entitlement as well as the 'beta' entitlement
+ *
+ * @param string|\App\User $user The user
+ * @param string $sku The beta SKU title
+ */
+ protected function assignBetaEntitlement($user, $sku): void
+ {
+ if (is_string($user)) {
+ $user = $this->getTestUser($user);
+ }
+
+ $user->assignSku(\App\Sku::where('title', 'beta')->first());
+ $user->assignSku(\App\Sku::where('title', $sku)->first());
+ }
+
protected function assertUserEntitlements($user, $expected)
{
// Assert the user entitlements
@@ -27,6 +44,15 @@
}
/**
+ * Removes all beta entitlements from the database
+ */
+ protected function clearBetaEntitlements(): void
+ {
+ $betas = \App\Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ \App\Entitlement::whereIn('sku_id', $betas)->delete();
+ }
+
+ /**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
diff --git a/src/tests/Unit/SignupCodeTest.php b/src/tests/Unit/SignupCodeTest.php
--- a/src/tests/Unit/SignupCodeTest.php
+++ b/src/tests/Unit/SignupCodeTest.php
@@ -18,6 +18,6 @@
$this->assertTrue(is_string($code));
$this->assertTrue(strlen($code) === env('SIGNUP_CODE_LENGTH', SignupCode::SHORTCODE_LENGTH));
- $this->assertTrue(strspn($code, env('SIGNUP_CODE_CHARS', SignupCode::SHORTCODE_CHARS)) === strlen($code));
+ $this->assertTrue(strspn($code, env('SIGNUP_CODE_CHARS', \App\Utils::CHARS)) === strlen($code));
}
}
diff --git a/src/tests/Unit/VerificationCodeTest.php b/src/tests/Unit/VerificationCodeTest.php
--- a/src/tests/Unit/VerificationCodeTest.php
+++ b/src/tests/Unit/VerificationCodeTest.php
@@ -17,10 +17,9 @@
$code = VerificationCode::generateShortCode();
$code_length = env('VERIFICATION_CODE_LENGTH', VerificationCode::SHORTCODE_LENGTH);
- $code_chars = env('VERIFICATION_CODE_CHARS', VerificationCode::SHORTCODE_CHARS);
$this->assertTrue(is_string($code));
$this->assertTrue(strlen($code) === $code_length);
- $this->assertTrue(strspn($code, $code_chars) === strlen($code));
+ $this->assertTrue(strspn($code, \App\Utils::CHARS) === strlen($code));
}
}
diff --git a/src/webpack.mix.js b/src/webpack.mix.js
--- a/src/webpack.mix.js
+++ b/src/webpack.mix.js
@@ -27,6 +27,7 @@
mix.js('resources/js/user.js', 'public/js')
.js('resources/js/admin.js', 'public/js')
+ .js('resources/js/meet.js', 'public/js')
glob.sync('resources/themes/*/', {}).forEach(fromDir => {
const toDir = fromDir.replace('resources/themes/', 'public/themes/')

File Metadata

Mime Type
text/plain
Expires
Thu, Oct 31, 8:21 AM (7 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
10061091
Default Alt Text
D1447.diff (436 KB)

Event Timeline