diff --git a/Dockerfile b/Dockerfile index 2c053a1ec..9230a1217 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM elixir:1.5 -RUN apt-get update -qq && apt-get install -y inotify-tools nodejs nodejs-legacy +RUN apt-get update -qq && apt-get install -y inotify-tools RUN curl -so- -L https://yarnpkg.com/install.sh | bash RUN mkdir /ret WORKDIR /ret diff --git a/Jenkinsfile b/Jenkinsfile index 475fc5b2a..748410fc2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,19 +1,5 @@ import groovy.json.JsonOutput -// From https://issues.jenkins-ci.org/browse/JENKINS-44231 - -// Given arbitrary string returns a strongly escaped shell string literal. -// I.e. it will be in single quotes which turns off interpolation of $(...), etc. -// E.g.: 1'2\3\'4 5"6 (groovy string) -> '1'\''2\3\'\''4 5"6' (groovy string which can be safely pasted into shell command). -def shellString(s) { - // Replace ' with '\'' (https://unix.stackexchange.com/a/187654/260156). Then enclose with '...'. - // 1) Why not replace \ with \\? Because '...' does not treat backslashes in a special way. - // 2) And why not use ANSI-C quoting? I.e. we could replace ' with \' - // and enclose using $'...' (https://stackoverflow.com/a/8254156/4839573). - // Because ANSI-C quoting is not yet supported by Dash (default shell in Ubuntu & Debian) (https://unix.stackexchange.com/a/371873). - '\'' + s.replace('\'', '\'\\\'\'') + '\'' -} - pipeline { agent any @@ -34,46 +20,65 @@ pipeline { /usr/bin/script --return -c \\\\"sudo /usr/bin/hab-docker-studio -k mozillareality run /bin/bash scripts/build.sh\\\\" /dev/null ''' - sh 'sudo /usr/bin/hab-pkg-upload $(ls -rt results/*.hart | head -n 1)' + sh 'sudo /usr/bin/hab-pkg-upload $(ls -t results/*.hart | head -n 1)' script { - // Grab IDENT file and cat it from .hart - def s = $/eval 'ls -rt results/*.hart | head -n 1'/$ - def hart = sh(returnStdout: true, script: "${s}").trim() - s = $/eval 'tail -n +6 ${hart} | xzcat | tar tf - | grep IDENT'/$ - def identPath = sh(returnStdout: true, script: "${s}").trim() - s = $/eval 'tail -n +6 ${hart} | xzcat | tar xf - "${identPath}" -O'/$ - def packageIdent = sh(returnStdout: true, script: "${s}").trim() + def poolHost = env.RET_DARK_POOL_HOST + def slackURL = env.SLACK_URL + def buildNumber = env.BUILD_NUMBER + def jobName = env.JOB_NAME + def disablePromoteToStable = env.DISABLE_PROMOTE_TO_STABLE + def showQAPromoteCommand = env.SHOW_QA_PROMOTE_COMMAND + def qaBuildsSlackChannel = env.QA_BUILDS_SLACK_CHANNEL + + // Grab IDENT file and cat it from .hart + def s = $/eval 'ls -t results/*.hart | head -n 1'/$ + def hart = sh(returnStdout: true, script: "${s}").trim() + + s = $/eval 'tail -n +6 ${hart} | xzcat | tar tf - | grep IDENT'/$ + def identPath = sh(returnStdout: true, script: "${s}").trim() + + s = $/eval 'tail -n +6 ${hart} | xzcat | tar xf - "${identPath}" -O'/$ + def packageIdent = sh(returnStdout: true, script: "${s}").trim() + + def gitMessage = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'[%an] %s'").trim() + def gitSha = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim() + + if (disablePromoteToStable == null || disablePromoteToStable == "") { + def retPool = sh(returnStdout: true, script: "curl https://${poolHost}/api/v1/meta | jq -r '.pool'").trim() + sh "sudo /usr/bin/hab-pkg-promote '${packageIdent}' '${retPool}'" + sh "sudo /usr/bin/hab-pkg-promote '${packageIdent}' 'stable'" + def packageTimeVersion = packageIdent.tokenize('/')[3] def (major, minor, version) = packageIdent.tokenize('/')[2].tokenize('.') def retVersion = "${major}.${minor}.${packageTimeVersion}" - def poolHost = env.RET_DARK_POOL_HOST - def retPool = sh(returnStdout: true, script: "curl https://${poolHost}/api/v1/meta | jq -r '.pool'").trim() + def retPoolIcon = retPool == 'earth' ? ':earth_americas:' : ':new_moon:' - def gitMessage = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'[%an] %s'").trim() - def gitSha = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim() - def slackURL = env.SLACK_URL def text = ( - "** *${env.JOB_NAME}* " + + "** *${jobName}* " + "\n" + " " + "Reticulum -> ${retPoolIcon} `${retPool}`: ```${gitSha} ${gitMessage}```\n" + " - to push:\n" + "`/mr ret deploy ${retVersion} ${retPool}`" ) - def payload = 'payload=' + JsonOutput.toJson([ - text : text, - channel : "#mr-builds", - username : "buildbot", - icon_emoji: ":gift:" - ]) - sh "sudo /usr/bin/hab-pkg-promote '${packageIdent}' '${retPool}'" - sh "sudo /usr/bin/hab-pkg-promote '${packageIdent}' 'stable'" - sh "curl -X POST --data-urlencode ${shellString(payload)} ${slackURL}" + sendSlackMessage(text, "#mr-builds", ":gift:", slackURL); + } - // Upload to ret depot after publishing to slack to minimize wait - sh 'sudo /usr/bin/hab-ret-pkg-upload $(ls -rt results/*.hart | head -n 1)' + // Upload to ret depot after publishing to slack to minimize wait + sh 'sudo /usr/bin/hab-ret-pkg-upload $(ls -t results/*.hart | head -n 1)' + + if (showQAPromoteCommand == "true") { + def text = ( + "** *${jobName}* " + + "\n" + + " " + + "${packageIdent} built and uploaded - to promote:\n" + + "`/mr promote-ret-qa ${packageIdent}`" + ) + sendSlackMessage(text, qaBuildsSlackChannel, ":gift:", slackURL); + } } } } @@ -85,3 +90,27 @@ pipeline { } } } + +def sendSlackMessage(text, channel, icon, slackURL) { + def payload = 'payload=' + JsonOutput.toJson([ + text : text, + channel : channel, + username : "buildbot", + icon_emoji: icon + ]) + sh "curl -X POST --data-urlencode ${shellString(payload)} ${slackURL}" +} + +// From https://issues.jenkins-ci.org/browse/JENKINS-44231 + +// Given arbitrary string returns a strongly escaped shell string literal. +// I.e. it will be in single quotes which turns off interpolation of $(...), etc. +// E.g.: 1'2\3\'4 5"6 (groovy string) -> '1'\''2\3\'\''4 5"6' (groovy string which can be safely pasted into shell command). +def shellString(s) { + // Replace ' with '\'' (https://unix.stackexchange.com/a/187654/260156). Then enclose with '...'. + // 1) Why not replace \ with \\? Because '...' does not treat backslashes in a special way. + // 2) And why not use ANSI-C quoting? I.e. we could replace ' with \' + // and enclose using $'...' (https://stackoverflow.com/a/8254156/4839573). + // Because ANSI-C quoting is not yet supported by Dash (default shell in Ubuntu & Debian) (https://unix.stackexchange.com/a/371873). + '\'' + s.replace('\'', '\'\\\'\'') + '\'' +} diff --git a/README.md b/README.md index 4d7fc0844..fd49a4a92 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,14 @@ A hybrid game networking and web API server, focused on Social Mixed Reality. #### PostgreSQL (recommended version 11.x): -Linux: Use your package manager +Linux: + +On Ubuntu, you can use +``` +apt install postgresql +``` + +Otherwise, consult your package manager of choice for other Linux distributions Windows: https://www.postgresql.org/download/windows/ @@ -30,8 +37,9 @@ Run the following commands at the root of the reticulum directory: 1. `mix deps.get` 2. `mix ecto.create` - - If step 2 fails, you may need to change the password for the `postgres` role to match the password configured `dev.exs`. - - From within the `psql` shell, enter `ALTER USER postgres WITH PASSWORD 'postgres';` + * If step 2 fails, you may need to change the password for the `postgres` role to match the password configured `dev.exs`. + * From within the `psql` shell, enter `ALTER USER postgres WITH PASSWORD 'postgres';` + * If you receive an error that the `ret_dev` database does not exist, (using psql again) enter `create database ret_dev;` 3. from the `assets` directory, `npm install` 4. From the project directory `mkdir -p storage/dev` @@ -41,19 +49,22 @@ Run `scripts/run.sh` if you have the hubs secret repo cloned. Otherwise `iex -S ## Run Hubs Against a Local Reticulum Instance -### 0. Dependencies - -[Install NodeJS](https://nodejs.org) if you haven't already. We recommend version 12 or above. - ### 1. Setup the `hubs.local` hostname When running the full stack for Hubs (which includes Reticulum) locally it is necessary to add a `hosts` entry pointing `hubs.local` to your local server's IP. This will allow the CSP checks to pass that are served up by Reticulum so you can test the whole app. Note that you must also load hubs.local over https. -Example: +On MacOS or Linux: +```bash +nano /etc/hosts ``` -hubs.local 127.0.0.1 + +From there, add a host alias + +Example: +```bash +127.0.0.1 hubs.local ``` ### 2. Setting up the Hubs Repository @@ -96,6 +107,17 @@ After you've started Reticulum for the first time you'll likely want to create a Ret.Account |> Ret.Repo.all() |> Enum.at(0) |> Ecto.Changeset.change(is_admin: true) |> Ret.Repo.update!() ``` +### 7. Start the Admin Portal server in local development mode + +When running locally, you will need to also run the admin portal, which routes to hubs.local:8989 +Using a separate terminal instance, navigate to the `hubs/admin` folder and use: +``` +npm run local +``` + +You can now navigate to https://hubs.local:4000/admin to access the admin control panel + + ## Run Spoke Against a Local Reticulum Instance 1. Follow the steps above to setup Hubs @@ -125,3 +147,5 @@ default_janus_csp_rule = ``` psql-userdb="host=hubs.local dbname=ret_dev user=postgres password=postgres options='-c search_path=coturn' connect_timeout=30" ``` + + diff --git a/assets/brunch-config.js b/assets/brunch-config.js deleted file mode 100644 index b15df4600..000000000 --- a/assets/brunch-config.js +++ /dev/null @@ -1,62 +0,0 @@ -exports.config = { - // See http://brunch.io/#documentation for docs. - files: { - javascripts: { - joinTo: "js/app.js" - - // To use a separate vendor.js bundle, specify two files path - // http://brunch.io/docs/config#-files- - // joinTo: { - // "js/app.js": /^js/, - // "js/vendor.js": /^(?!js)/ - // } - // - // To change the order of concatenation of files, explicitly mention here - // order: { - // before: [ - // "vendor/js/jquery-2.1.1.js", - // "vendor/js/bootstrap.min.js" - // ] - // } - }, - stylesheets: { - joinTo: "css/app.css" - }, - templates: { - joinTo: "js/app.js" - } - }, - - conventions: { - // This option sets where we should place non-css and non-js assets in. - // By default, we set this to "/assets/static". Files in this directory - // will be copied to `paths.public`, which is "priv/static" by default. - assets: /^(static)/ - }, - - // Phoenix paths configuration - paths: { - // Dependencies and current project directories to watch - watched: ["static", "css", "js", "vendor"], - // Where to compile files to - public: "../priv/static" - }, - - // Configure your plugins - plugins: { - babel: { - // Do not use ES6 compiler in vendor code - ignore: [/vendor/] - } - }, - - modules: { - autoRequire: { - "js/app.js": ["js/app"] - } - }, - - npm: { - enabled: true - } -}; diff --git a/assets/css/app.css b/assets/css/app.css deleted file mode 100644 index 48dc39b66..000000000 --- a/assets/css/app.css +++ /dev/null @@ -1,6 +0,0 @@ -/* This file is for your main application css. */ - -/* Phoenix flash messages */ -.alert:empty { display: none; } - -.center { text-align: center; } \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js deleted file mode 100644 index cc75f8ed2..000000000 --- a/assets/js/app.js +++ /dev/null @@ -1,26 +0,0 @@ -// Brunch automatically concatenates all files in your -// watched paths. Those paths can be configured at -// config.paths.watched in "brunch-config.js". -// -// However, those files will only be executed if -// explicitly imported. The only exception are files -// in vendor, which are never wrapped in imports and -// therefore are always executed. - -// Import dependencies -// -// If you no longer want to use a dependency, remember -// to also remove its path from "config.paths.watched". -import "phoenix_html" - - -// Import local files -// -// Local files can be imported directly using relative -// paths "./socket" or full ones "web/static/js/socket". - -// import socket from "./socket" - -import Chat from "./chat" - -Chat.init() diff --git a/assets/js/chat.js b/assets/js/chat.js deleted file mode 100644 index e40e9edd7..000000000 --- a/assets/js/chat.js +++ /dev/null @@ -1,156 +0,0 @@ -import {Socket, Presence} from "phoenix" - -let Chat = { - - init() { - if (!window.username) { return } - // Socket - let socket = new Socket("/socket", { - params: { - username: window.username, - room_id: window.room_id - } - }) - socket.connect() - - // Presence - let presences = {} - let globaPresences = {} - - let formatTimestamp = (timestamp) => { - let date = new Date(timestamp) - return date.toLocaleTimeString() - } - - let listBy = (user, {metas: metas}) => { - return { - user: user, - onlineAt: formatTimestamp(metas[0].online_at) - } - } - - let userList = document.getElementById("UserList") - let globalUserList = document.getElementById("GlobalUserList") - - let render = (presences, list) => { - list['innerHTML'] = Presence.list(presences, listBy) - .map(presence => ` -
  • - ${presence.user} -
    online since ${presence.onlineAt} -
  • - `) - .join("") - - Presence.list(presences, listBy).forEach(presence => { - document.getElementById(list.id + ":" + presence.user).addEventListener("click", (e) => { - messageInput.value = "/" + presence.user + " " + messageInput.value - messageInput.focus() - }) - }) - } - - // Channels - let room = socket.channel("room:" + window.room_id) - let globalAll = socket.channel("global:all") - let globalUser = socket.channel("global:" + window.username) - let globalUsers = {} - - globalUser.join() - - room.on("presence_state", state => { - presences = Presence.syncState(presences, state) - render(presences, userList) - }) - - room.on("presence_diff", diff => { - presences = Presence.syncDiff(presences, diff) - render(presences, userList) - }) - - room.join() - - globalAll.on("presence_state", state => { - globaPresences = Presence.syncState(globaPresences, state) - render(globaPresences, globalUserList) - }) - - globalAll.on("presence_diff", diff => { - globaPresences = Presence.syncDiff(globaPresences, diff) - render(globaPresences, globalUserList) - }) - - globalAll.join() - - //Messages - let messageInput = document.getElementById("NewMessage") - - messageInput.addEventListener("keypress", (e) => { - if (e.keyCode == 13 && messageInput.value != "") { - - if(messageInput.value.charAt(0) == '/') { - let emailString = messageInput.value.split(' ')[0].substring(1); - - if(!globalUsers[emailString]) { - let topic = "global:" + emailString - globalUsers[emailString] = socket.channel(topic) - globalUsers[emailString].join() - globalUsers[emailString].on("message:new", message => renderMessage(message, false)) - } - - let date = new Date() - let message = { - body: messageInput.value.substring(emailString.length + 1), - receiver: emailString, - sender: window.username, - timestamp: date - } - renderMessage(message, true, true) - globalUsers[emailString].push("message:new", message) - - } else { - room.push("message:new", messageInput.value) - } - messageInput.value = "" - } - }) - - let messageList = document.getElementById("MessageList") - - let renderMessage = (message, isDirect = false, isSender = false) => { - let messageElement = document.createElement("li") - if (isDirect) { - messageElement.innerHTML = ` - (DM) ${isSender ? "to: " + message.receiver : "from: " + message.sender} - ${formatTimestamp(message.timestamp)} -

    ${message.body}

    - ` - messageElement.addEventListener("click", (e) => { - messageInput.value = "/" + (isSender ? message.receiver : message.sender) + " " + messageInput.value - messageInput.focus() - }) - } else { - messageElement.innerHTML = ` - ${message.user} - ${formatTimestamp(message.timestamp)} -

    ${message.body}

    - ` - messageElement.addEventListener("click", (e) => { - messageInput.value = "/" + message.user + " " + messageInput.value - messageInput.focus() - }) - } - - - - messageList.appendChild(messageElement) - messageList.scrollTop = messageList.scrollHeight; - } - - room.on("message:new", message => renderMessage(message)) - - globalUser.on("message:new", message => renderMessage(message, true)) - } -} - -export default Chat \ No newline at end of file diff --git a/assets/js/socket.js b/assets/js/socket.js deleted file mode 100644 index 5c23a67f7..000000000 --- a/assets/js/socket.js +++ /dev/null @@ -1,62 +0,0 @@ -// NOTE: The contents of this file will only be executed if -// you uncomment its entry in "assets/js/app.js". - -// To use Phoenix channels, the first step is to import Socket -// and connect at the socket path in "lib/web/endpoint.ex": -import {Socket} from "phoenix" - -let socket = new Socket("/socket", {params: {token: window.userToken}}) - -// When you connect, you'll often need to authenticate the client. -// For example, imagine you have an authentication plug, `MyAuth`, -// which authenticates the session and assigns a `:current_user`. -// If the current user exists you can assign the user's token in -// the connection for use in the layout. -// -// In your "lib/web/router.ex": -// -// pipeline :browser do -// ... -// plug MyAuth -// plug :put_user_token -// end -// -// defp put_user_token(conn, _) do -// if current_user = conn.assigns[:current_user] do -// token = Phoenix.Token.sign(conn, "user socket", current_user.id) -// assign(conn, :user_token, token) -// else -// conn -// end -// end -// -// Now you need to pass this token to JavaScript. You can do so -// inside a script tag in "lib/web/templates/layout/app.html.eex": -// -// -// -// You will need to verify the user token in the "connect/2" function -// in "lib/web/channels/user_socket.ex": -// -// def connect(%{"token" => token}, socket) do -// # max_age: 1209600 is equivalent to two weeks in seconds -// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do -// {:ok, user_id} -> -// {:ok, assign(socket, :user, user_id)} -// {:error, reason} -> -// :error -// end -// end -// -// Finally, pass the token on connect as below. Or remove it -// from connect if you don't care about authentication. - -socket.connect() - -// Now that you are connected, you can join channels with a topic: -let channel = socket.channel("topic:subtopic", {}) -channel.join() - .receive("ok", resp => { console.log("Joined successfully", resp) }) - .receive("error", resp => { console.log("Unable to join", resp) }) - -export default socket diff --git a/assets/package.json b/assets/package.json deleted file mode 100644 index 5d1e2dd2f..000000000 --- a/assets/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "repository": {}, - "license": "MIT", - "scripts": { - "build": "brunch build --production", - "watch": "brunch watch --stdin" - }, - "dependencies": { - "phoenix": "file:../deps/phoenix", - "phoenix_html": "file:../deps/phoenix_html" - }, - "devDependencies": { - "babel-brunch": "6.1.1", - "brunch": "2.10.9", - "clean-css-brunch": "2.10.0", - "uglify-js-brunch": "2.10.0" - } -} diff --git a/assets/yarn.lock b/assets/yarn.lock deleted file mode 100644 index 9bfb04d3f..000000000 --- a/assets/yarn.lock +++ /dev/null @@ -1,2776 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - -accepts@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" - dependencies: - mime-types "~2.1.16" - negotiator "0.6.1" - -acorn@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" - -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - -anymatch@^1.0.0, anymatch@^1.3.0, anymatch@~1.3, anymatch@~1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - -anysort@~1.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/anysort/-/anysort-1.0.1.tgz#341bd5d5ba1485f64e55ae865f1d45994b507fc4" - dependencies: - anymatch "~1.3.0" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - -are-we-there-yet@~1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - dependencies: - arr-flatten "^1.0.1" - -arr-flatten@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - -asn1.js@^4.0.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - -assert@~1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.3.0.tgz#03939a622582a812cc202320a0b9a56c9b815849" - dependencies: - util "0.10.3" - -async-each@^1.0.0, async-each@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - -async@~0.2.6: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - -babel-brunch@6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-brunch/-/babel-brunch-6.1.1.tgz#0b9d04c1df12f66e76a4dbdee34d8734876f1877" - dependencies: - anymatch "^1.0.0" - babel-core "^6.0.0" - babel-preset-latest "^6.22.0" - loggy "~1.0.1" - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.0.0, babel-core@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.0" - debug "^2.6.8" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.7" - slash "^1.0.0" - source-map "^0.5.6" - -babel-generator@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.6" - trim-right "^1.0.1" - -babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" - dependencies: - babel-helper-explode-assignable-expression "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-explode-assignable-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-remap-async-to-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-syntax-async-functions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" - -babel-plugin-syntax-exponentiation-operator@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" - -babel-plugin-syntax-trailing-function-commas@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" - -babel-plugin-transform-async-to-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" - dependencies: - babel-helper-remap-async-to-generator "^6.24.1" - babel-plugin-syntax-async-functions "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" - dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-plugin-transform-es2015-classes@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-for-of@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.22.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-exponentiation-operator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" - dependencies: - babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" - babel-plugin-syntax-exponentiation-operator "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-regenerator@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" - dependencies: - regenerator-transform "^0.10.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-preset-es2015@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.24.1" - babel-plugin-transform-es2015-classes "^6.24.1" - babel-plugin-transform-es2015-computed-properties "^6.24.1" - babel-plugin-transform-es2015-destructuring "^6.22.0" - babel-plugin-transform-es2015-duplicate-keys "^6.24.1" - babel-plugin-transform-es2015-for-of "^6.22.0" - babel-plugin-transform-es2015-function-name "^6.24.1" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-plugin-transform-es2015-modules-systemjs "^6.24.1" - babel-plugin-transform-es2015-modules-umd "^6.24.1" - babel-plugin-transform-es2015-object-super "^6.24.1" - babel-plugin-transform-es2015-parameters "^6.24.1" - babel-plugin-transform-es2015-shorthand-properties "^6.24.1" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.24.1" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.22.0" - babel-plugin-transform-es2015-unicode-regex "^6.24.1" - babel-plugin-transform-regenerator "^6.24.1" - -babel-preset-es2016@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz#f900bf93e2ebc0d276df9b8ab59724ebfd959f8b" - dependencies: - babel-plugin-transform-exponentiation-operator "^6.24.1" - -babel-preset-es2017@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.24.1.tgz#597beadfb9f7f208bcfd8a12e9b2b29b8b2f14d1" - dependencies: - babel-plugin-syntax-trailing-function-commas "^6.22.0" - babel-plugin-transform-async-to-generator "^6.24.1" - -babel-preset-latest@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.1.tgz#677de069154a7485c2d25c577c02f624b85b85e8" - dependencies: - babel-preset-es2015 "^6.24.1" - babel-preset-es2016 "^6.24.1" - babel-preset-es2017 "^6.24.1" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - -base64-js@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - dependencies: - tweetnacl "^0.14.3" - -binary-extensions@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - dependencies: - inherits "~2.0.0" - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - -body-parser@1.18.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" - dependencies: - bytes "3.0.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.1" - http-errors "~1.6.2" - iconv-lite "0.4.19" - on-finished "~2.3.0" - qs "6.5.1" - raw-body "2.3.2" - type-is "~1.6.15" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - -bower-config@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.1.tgz#85fd9df367c2b8dbbd0caa4c5f2bad40cd84c2cc" - dependencies: - graceful-fs "^4.1.3" - mout "^1.0.0" - optimist "^0.6.1" - osenv "^0.1.3" - untildify "^2.1.0" - -brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - -browser-resolve@^1.11.1: - version "1.11.2" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" - dependencies: - resolve "1.1.7" - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" - dependencies: - 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@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - -browserify-rsa@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - dependencies: - 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" - -brunch-skeletons@~0.1.4: - version "0.1.6" - resolved "https://registry.yarnpkg.com/brunch-skeletons/-/brunch-skeletons-0.1.6.tgz#f15102a1b6babc964ce3d7e3c141dc0daa9d28ac" - -brunch@2.10.9: - version "2.10.9" - resolved "https://registry.yarnpkg.com/brunch/-/brunch-2.10.9.tgz#a74bb5aef87fa87ce0dd4ca4f996610204358d30" - dependencies: - anymatch "~1.3" - anysort "~1.0" - check-dependencies "~1.0.1" - chokidar "^1.6" - coffee-script "~1.11" - commander "~2.9" - commonjs-require-definition "~0.6.2" - debug "~2.2" - deppack "~0.7" - deps-install "~0.1" - fcache "~0.3" - init-skeleton "~1.0" - loggy "~1.0.2" - micro-es7-shim "^0.1" - micro-promisify "~0.1" - mkdirp "~0.5" - promise.prototype.finally "^2" - read-components "~0.7" - serve-brunch "~0.2" - since-app-start "~0.3" - skemata "~0.1" - source-map "~0.5" - universal-path "^0.1" - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - -buffer@~4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.3.1.tgz#0e65fd01cc3e9154d152f6b3c934b5b8a1b6733c" - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz#6f22003baacf003ccd287afe6872151fddc58579" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - 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" - -check-dependencies@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/check-dependencies/-/check-dependencies-1.0.1.tgz#9e7f15822de20621ec6b9ffaabac4d588c3811b0" - dependencies: - bower-config "^1.4.0" - chalk "^1.1.3" - findup-sync "^0.4.2" - lodash.camelcase "^4.3.0" - minimist "^1.2.0" - semver "^5.3.0" - -chokidar@^1.6: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -clean-css-brunch@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/clean-css-brunch/-/clean-css-brunch-2.10.0.tgz#6a151e6509589cd341dce04981a1c75062a22d18" - dependencies: - clean-css "~4.0" - -clean-css@~4.0: - version "4.0.13" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.0.13.tgz#feb2a176062d72a6c3e624d9213cac6a0c485e80" - dependencies: - source-map "0.5.x" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - -coffee-script@~1.11: - version "1.11.1" - resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.11.1.tgz#bf1c47ad64443a0d95d12df2b147cc0a4daad6e9" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" - -commander@^2.0.0: - version "2.12.2" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" - -commander@~2.9: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - dependencies: - graceful-readlink ">= 1.0.0" - -commonjs-require-definition@~0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/commonjs-require-definition/-/commonjs-require-definition-0.6.2.tgz#1b66a1babe602605c1ee0a6d86e2e26799ca7cec" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -connect-slashes@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/connect-slashes/-/connect-slashes-1.3.1.tgz#95d61830d0f9d5853c8688f0b5f43988b186ac37" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - -convert-source-map@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - -core-js@^2.4.0, core-js@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -create-ecdh@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" - dependencies: - bn.js "^4.1.0" - elliptic "^6.0.0" - -create-hash@^1.1.0, create-hash@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - ripemd160 "^2.0.0" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" - dependencies: - 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" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - -crypto-browserify@~3.11.0: - version "3.11.1" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" - dependencies: - 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" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -debug@2.6.9, debug@^2.2, debug@^2.2.0, debug@^2.6.8: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - dependencies: - ms "2.0.0" - -debug@~2.2, debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - -decamelize@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -deep-assign@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/deep-assign/-/deep-assign-2.0.0.tgz#ebe06b1f07f08dae597620e3dd1622f371a1c572" - dependencies: - is-obj "^1.0.0" - -deep-extend@~0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" - -define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" - dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" - -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -depd@1.1.1, depd@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" - -deppack@~0.7: - version "0.7.0" - resolved "https://registry.yarnpkg.com/deppack/-/deppack-0.7.0.tgz#fbb84f9a531ace2f28289092afc0568295c790b7" - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - browser-resolve "^1.11.1" - deep-assign "^2.0.0" - detective "^4.3.1" - glob "^7.0.3" - loggy "~0.3.0" - micro-promisify "^0.1.1" - node-browser-modules "^0.1.0" - "true-case-path" "^1.0.2" - -deps-install@~0.1, deps-install@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/deps-install/-/deps-install-0.1.1.tgz#324af2e617ac04fb7ef4be7d6099ea3a102f6119" - dependencies: - loggy "^1" - micro-promisify "~0.1.0" - -des.js@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -detect-file@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" - dependencies: - fs-exists-sync "^0.1.0" - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - dependencies: - repeating "^2.0.0" - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - -detective@^4.3.1: - version "4.7.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.0.tgz#6276e150f9e50829ad1f90ace4d9a2304188afcf" - dependencies: - acorn "^5.2.1" - defined "^1.0.0" - -diffie-hellman@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -domain-browser@~1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -elliptic@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" - dependencies: - 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" - -encodeurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" - -es-abstract@^1.6.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-to-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" - dependencies: - is-callable "^1.1.1" - is-date-object "^1.0.1" - is-symbol "^1.0.1" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - -events@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - dependencies: - is-posix-bracket "^0.1.0" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - dependencies: - fill-range "^2.1.0" - -expand-tilde@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" - dependencies: - os-homedir "^1.0.1" - -express@^4.0.0: - version "4.16.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" - dependencies: - accepts "~1.3.4" - array-flatten "1.1.1" - body-parser "1.18.2" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.1" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.0" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.2" - qs "6.5.1" - range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.1" - serve-static "1.13.1" - setprototypeof "1.1.0" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@~3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - dependencies: - is-extglob "^1.0.0" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - -fast-levenshtein@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" - -fcache@~0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/fcache/-/fcache-0.3.0.tgz#d45f2f908642b91b798e88195ec47881a51c3d44" - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - -fill-range@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^1.1.3" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - -findup-sync@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" - dependencies: - detect-file "^0.1.0" - is-glob "^2.0.1" - micromatch "^2.3.7" - resolve-dir "^0.1.0" - -for-in@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - dependencies: - for-in "^1.0.1" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - -fs-exists-sync@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -fsevents@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" - dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.39" - -fstream-ignore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: - version "1.0.11" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - dependencies: - 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" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - dependencies: - assert-plus "^1.0.0" - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - dependencies: - is-glob "^2.0.0" - -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.3, glob@^7.0.5: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - 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" - -global-modules@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" - dependencies: - global-prefix "^0.1.4" - is-windows "^0.2.0" - -global-prefix@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" - dependencies: - homedir-polyfill "^1.0.0" - ini "^1.3.4" - is-windows "^0.2.0" - which "^1.2.12" - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - -graceful-fs@^4.1.2, graceful-fs@^4.1.3: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - -growl@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.8.1.tgz#4b2dec8d907e93db336624dcec0183502f8c9428" - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - -has@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" - dependencies: - function-bind "^1.0.2" - -hash-base@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" - dependencies: - inherits "^2.0.1" - -hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.0" - -hawk@3.1.3, hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -homedir-polyfill@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - dependencies: - parse-passwd "^1.0.0" - -hosted-git-info@~2.1.4: - version "2.1.5" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" - -http-errors@1.6.2, http-errors@~1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" - dependencies: - depd "1.1.1" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-browserify@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" - -iconv-lite@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - -ieee754@^1.1.4: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" - -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - -ini@^1.3.4, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - -init-skeleton@~1.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/init-skeleton/-/init-skeleton-1.0.1.tgz#310ab730ad3a54b03dc4b08d699d576674ae11c8" - dependencies: - brunch-skeletons "~0.1.4" - deps-install "~0.1.0" - hosted-git-info "~2.1.4" - micro-promisify "~0.1.0" - mkdirp "~0.5.0" - ncp "^2.0.0" - normalize-git-url "~3.0.1" - -invariant@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" - dependencies: - loose-envify "^1.0.0" - -ipaddr.js@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - -is-callable@^1.1.1, is-callable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - dependencies: - is-extglob "^1.0.0" - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - dependencies: - kind-of "^3.0.2" - -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - dependencies: - has "^1.0.1" - -is-symbol@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -is-windows@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - dependencies: - isarray "1.0.0" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -js-tokens@^3.0.0, js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -kind-of@^3.0.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - dependencies: - is-buffer "^1.1.5" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - -lodash@^4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -loggy@^1, loggy@~1.0.1, loggy@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/loggy/-/loggy-1.0.2.tgz#d0ca31c421395f8c462d04870dd2228de3c4e219" - dependencies: - chalk "^1.1.1" - native-notifier "~0.1.0" - -loggy@~0.3.0: - version "0.3.5" - resolved "https://registry.yarnpkg.com/loggy/-/loggy-0.3.5.tgz#33f12801b1f6063966ea79d9b6a25db8fcbc4107" - dependencies: - ansicolors "~0.3.2" - growl "~1.8.1" - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - -loose-envify@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - dependencies: - js-tokens "^3.0.0" - -md5.js@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -micro-es7-shim@^0.1: - version "0.1.0" - resolved "https://registry.yarnpkg.com/micro-es7-shim/-/micro-es7-shim-0.1.0.tgz#aa9989a2f93037a93e7b30e2c92433db0b4ca228" - -micro-promisify@^0.1.1, micro-promisify@~0.1, micro-promisify@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/micro-promisify/-/micro-promisify-0.1.1.tgz#071da590b4956560dedf4aae7044729c1a28902d" - -micromatch@^2.1.5, micromatch@^2.3.7: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - -mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" - -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - -minimalistic-assert@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - -"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5, mkdirp@~0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mout@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mout/-/mout-1.1.0.tgz#0b29d41e6a80fa9e2d4a5be9d602e1d9d02177f6" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -nan@^2.3.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - -native-notifier@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/native-notifier/-/native-notifier-0.1.1.tgz#0f719731a410a7a243409eaba10a1446c2df831a" - dependencies: - tag-shell "~0.1.0" - -ncp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -node-browser-modules@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/node-browser-modules/-/node-browser-modules-0.1.0.tgz#4193cbb70f542759a5e4e6d6b01ccb638e99a1ac" - dependencies: - assert "~1.3.0" - buffer "~4.3.0" - crypto-browserify "~3.11.0" - domain-browser "~1.1.7" - events "~1.1.0" - https-browserify "~0.0.1" - os-browserify "~0.2.0" - path-browserify "~0.0.0" - process "~0.11.2" - punycode "~1.4.0" - querystring-es3 "~0.2.1" - readable-stream "~2.0.5" - stream-browserify "~2.0.1" - stream-http "~2.1.0" - string_decoder "~0.10.31" - timers-browserify "~1.4.2" - tty-browserify "~0.0.0" - url "~0.11.0" - util "~0.10.3" - vm-browserify "~0.0.4" - -node-pre-gyp@^0.6.39: - version "0.6.39" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" - dependencies: - detect-libc "^1.0.2" - hawk "3.1.3" - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-git-url@~3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/normalize-git-url/-/normalize-git-url-3.0.2.tgz#8e5f14be0bdaedb73e07200310aa416c27350fc4" - -normalize-path@^2.0.0, normalize-path@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - dependencies: - remove-trailing-separator "^1.0.1" - -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - dependencies: - 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@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -oauth-sign@~0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -object-keys@^1.0.8: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -os-browserify@~0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" - -os-homedir@^1.0.0, os-homedir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - -osenv@^0.1.3, osenv@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -parse-asn1@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" - dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - -parseurl@~1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - -path-browserify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - -pbkdf2@^3.0.3: - version "3.0.14" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - -"phoenix@file:../deps/phoenix": - version "1.3.0" - -"phoenix_html@file:../deps/phoenix_html": - version "2.10.4" - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - -private@^0.1.6, private@^0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -process@~0.11.0, process@~0.11.2: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - -promise.prototype.finally@^2: - version "2.0.1" - resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-2.0.1.tgz#b70d44ceb3825fa88004b5d1fbd906b3b7d9b758" - dependencies: - define-properties "^1.1.2" - es-abstract "^1.6.1" - function-bind "^1.1.0" - -proxy-addr@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.5.2" - -public-encrypt@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - -punycode@^1.4.1, punycode@~1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -pushserve@^1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pushserve/-/pushserve-1.0.2.tgz#fdfb803939ca33752d38f2d1aa975ac01a93253f" - dependencies: - commander "^2.0.0" - connect-slashes "^1.3.1" - express "^4.0.0" - serve-static "^1.10.0" - -qs@6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -querystring-es3@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - -randomatic@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -randombytes@^2.0.0, randombytes@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" - dependencies: - safe-buffer "^5.1.0" - -range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - -raw-body@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" - dependencies: - bytes "3.0.0" - http-errors "1.6.2" - iconv-lite "0.4.19" - unpipe "1.0.0" - -rc@^1.1.7: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-components@~0.7: - version "0.7.0" - resolved "https://registry.yarnpkg.com/read-components/-/read-components-0.7.0.tgz#77dce7adcb72a514240c47a675b9bcf7a3509dd9" - dependencies: - async-each "~1.0.0" - -readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4: - version "2.3.3" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - safe-buffer "~5.1.1" - string_decoder "~1.0.3" - util-deprecate "~1.0.1" - -readable-stream@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readdirp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" - dependencies: - graceful-fs "^4.1.2" - minimatch "^3.0.2" - readable-stream "^2.0.2" - set-immediate-shim "^1.0.1" - -regenerate@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.1.6" - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - dependencies: - is-equal-shallow "^0.1.3" - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - dependencies: - jsesc "~0.5.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - -repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - -request@2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -resolve-dir@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" - dependencies: - expand-tilde "^1.2.2" - global-modules "^0.2.3" - -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - dependencies: - align-text "^0.1.1" - -rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - dependencies: - glob "^7.0.5" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" - dependencies: - hash-base "^2.0.0" - inherits "^2.0.1" - -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - -semver@^5.3.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - -send@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" - dependencies: - debug "2.6.9" - depd "~1.1.1" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - -serve-brunch@~0.2: - version "0.2.0" - resolved "https://registry.yarnpkg.com/serve-brunch/-/serve-brunch-0.2.0.tgz#2f9b750ba898983ef0838b94e38ea4eed88e3c55" - dependencies: - debug "^2.2" - loggy "^1" - pushserve "^1" - -serve-static@1.13.1, serve-static@^1.10.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.1" - -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.9" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -since-app-start@~0.3: - version "0.3.2" - resolved "https://registry.yarnpkg.com/since-app-start/-/since-app-start-0.3.2.tgz#c59158fbfc86a5dc23bbdd7c4de1480fbb55d089" - dependencies: - debug "~2.2.0" - -skemata@~0.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/skemata/-/skemata-0.1.2.tgz#f3c521029b67be6e0825f0df867ee97916de4530" - dependencies: - fast-levenshtein "^1.1.3" - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - dependencies: - source-map "^0.5.6" - -source-map@0.5.x, source-map@^0.5.6, source-map@~0.5, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - -sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -"statuses@>= 1.3.1 < 2": - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - -statuses@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - -stream-browserify@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.1.1.tgz#3b880303babe036d6f6b43127d4dcd6f8893e1db" - dependencies: - builtin-status-codes "^2.0.0" - inherits "^2.0.1" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string_decoder@~0.10.31, string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" - dependencies: - safe-buffer "~5.1.0" - -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -tag-shell@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/tag-shell/-/tag-shell-0.1.0.tgz#e743816e6a6e805ac3735f4162e016b97a7ddfbd" - -tar-pack@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" - dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" - -tar@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" - dependencies: - block-stream "*" - fstream "^1.0.2" - inherits "2" - -timers-browserify@~1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" - dependencies: - process "~0.11.0" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - -tough-cookie@~2.3.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - dependencies: - punycode "^1.4.1" - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - -"true-case-path@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" - dependencies: - glob "^6.0.4" - -tty-browserify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -type-is@~1.6.15: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.15" - -uglify-js-brunch@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/uglify-js-brunch/-/uglify-js-brunch-2.10.0.tgz#60cd0fb652887a02ce6abcd1588de55dcc346f05" - dependencies: - uglify-js "~2.6.1" - -uglify-js@~2.6.1: - version "2.6.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.6.4.tgz#65ea2fb3059c9394692f15fed87c2b36c16b9adf" - dependencies: - async "~0.2.6" - source-map "~0.5.1" - uglify-to-browserify "~1.0.0" - yargs "~3.10.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - -uid-number@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - -universal-path@^0.1: - version "0.1.0" - resolved "https://registry.yarnpkg.com/universal-path/-/universal-path-0.1.0.tgz#0fca24c936ea3d2282013d143710c06687ed0677" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - -untildify@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" - dependencies: - os-homedir "^1.0.0" - -url@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -util@0.10.3, util@~0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - dependencies: - inherits "2.0.1" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - -uuid@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vm-browserify@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" - dependencies: - indexof "0.0.1" - -which@^1.2.12: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" - dependencies: - string-width "^1.0.2" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" diff --git a/config/config.exs b/config/config.exs index e9063a646..ca592de57 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,6 +28,7 @@ config :mime, :types, %{ # Configures the endpoint config :ret, RetWeb.Endpoint, url: [host: "localhost"], + # This config value is for local development only. secret_key_base: "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+7GHLKqt0gR9qzgThxb5", render_errors: [view: RetWeb.ErrorView, accepts: ~w(html json)], pubsub: [name: Ret.PubSub, adapter: Phoenix.PubSub.PG2] diff --git a/config/dev.exs b/config/dev.exs index 6cf1fd03a..02507eb78 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -25,8 +25,8 @@ config :ret, RetWeb.Endpoint, port: 4000, otp_app: :ret, cipher_suite: :strong, - keyfile: "#{System.get_env("PWD")}/priv/dev-ssl.key", - certfile: "#{System.get_env("PWD")}/priv/dev-ssl.cert" + keyfile: "#{File.cwd!()}/priv/dev-ssl.key", + certfile: "#{File.cwd!()}/priv/dev-ssl.cert" ], cors_proxy_url: [scheme: "https", host: cors_proxy_host, port: 4000], assets_url: [scheme: "https", host: assets_host, port: 4000], @@ -35,17 +35,10 @@ config :ret, RetWeb.Endpoint, debug_errors: true, code_reloader: true, check_origin: false, + # This config value is for local development only. secret_key_base: "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+7GHLKqt0gR9qzgThxb5", allowed_origins: "*", - allow_crawlers: true, - watchers: [ - node: [ - "node_modules/brunch/bin/brunch", - "watch", - "--stdin", - cd: Path.expand("../assets", __DIR__) - ] - ] + allow_crawlers: true # ## SSL Support # @@ -123,6 +116,7 @@ config :ret, Ret.DiscordClient, config :cors_plug, origin: ["*"] config :ret, + # This config value is for local development only. upload_encryption_key: "a8dedeb57adafa7821027d546f016efef5a501bd", bot_access_key: "" @@ -184,12 +178,14 @@ config :ret, Ret.OAuthToken, oauth_token_key: "" config :ret, Ret.Guardian, issuer: "ret", + # This config value is for local development only. secret_key: "47iqPEdWcfE7xRnyaxKDLt9OGEtkQG3SycHBEMOuT2qARmoESnhc76IgCUjaQIwX", ttl: {12, :weeks} config :web_push_encryption, :vapid_details, subject: "mailto:admin@mozilla.com", public_key: "BAb03820kHYuqIvtP6QuCKZRshvv_zp5eDtqkuwCUAxASBZMQbFZXzv8kjYOuLGF16A3k8qYnIN10_4asB-Aw7w", + # This config value is for local development only. private_key: "w76tXh1d3RBdVQ5eINevXRwW6Ow6uRcBa8tBDOXfmxM" config :sentry, diff --git a/config/test.exs b/config/test.exs index 6af51574e..7cbd65a06 100644 --- a/config/test.exs +++ b/config/test.exs @@ -5,6 +5,7 @@ use Mix.Config config :ret, RetWeb.Endpoint, http: [port: 4001], allowed_origins: "*", + # This config value is for local development only. secret_key_base: "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+4GHLKqt0gR9qzgThxa5", cors_proxy_url: [scheme: "https", host: "hubs-proxy.local", port: 4000], server: false @@ -45,6 +46,7 @@ config :ret, Ret.Locking, config :ret, Ret.Guardian, issuer: "ret", + # This config value is for local development only. secret_key: "47iqPEdWcfE7xRnyaxKDLt9OGEtkQG3SycHBEMOuT2qARmoESnhc76IgCUjaQIwX" config :ret, Ret.Storage, @@ -64,5 +66,6 @@ config :ret, Ret.Locking, lock_timeout_ms: 1000 * 60 * 15 config :ret, Ret.Account, admin_email: "admin@mozilla.com" config :ret, Ret.PermsToken, + # This config value is for local development only. perms_key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpgIBAAKCAQEA3RY0qLmdthY6Q0RZ4oyNQSL035BmYLNdleX1qVpG1zfQeLWf\n/otgc8Ho2w8y5wW2W5vpI4a0aexNV2evgfsZKtx0q5WWwjsr2xy0Ak1zhWTgZD+F\noHVGJ0xeFse2PnEhrtWalLacTza5RKEJskbNiTTu4fD+UfOCMctlwudNSs+AkmiP\nSxc8nWrZ5BuvdnEXcJOuw0h4oyyUlkmj+Oa/ZQVH44lmPI9Ih0OakXWpIfOob3X0\nXqcdywlMVI2hzBR3JNodRjyEz33p6E//lY4Iodw9NdcRpohGcxcgQ5vf4r4epLIa\ncr0y5w1ZiRyf6BwyqJ6IBpA7yYpws3r9qxmAqwIDAQABAoIBAQCgwy/hbK9wo3MU\nTNRrdzaTob6b/l1jfanUgRYEYl/WyYAu9ir0JhcptVwERmYGNVIoBRQfQClaSHjo\n0L1/b74aO5oe1rR8Yhh+yL1gWz9gRT0hyEr7paswkkhsmiY7+3m5rxsrfinlM+6+\nJ7dsSi3U0ofOBbZ4kvAeEz/Y3OaIOUbQraP312hQnTVQ3kp7HNi9GcLK9rq2mASu\nO0DxDHXdZMsRN1K4tOKRZDsKGAEfL2jKN7+ndvsDhb4mAQaVKM8iw+g5O4HDA8uB\nmwycaWhjilZWEyUyqvXE8tOMLS59sq6i1qrf8zIMWDOizebF/wnrQ42kzt5kQ0ZJ\nwCPOC3sxAoGBAO6KfWr6WsXD6phnjVXXi+1j3azRKJGQorwQ6K3bXmISdlahngas\nmBGBmI7jYTrPPeXAHUbARo/zLcbuGCf1sPipkAHYVC8f9aUbA205BREB15jNyXr3\nXzhR/ronbn0VeR9iRua2FZjVChz22fdz9MvRJiinP8agYIQ4LovDk3lzAoGBAO1E\nrZpOuv3TMQffPaPemWuvMYfZLgx2/AklgYqSoi683vid9HEEAdVzNWMRrOg0w5EH\nWMEMPwJTYvy3xIgcFmezk5RMHTX2J32JzDJ8Y/uGf1wMrdkt3LkPRfuGepEDDtBa\nrUSO/MeGXLu5p8QByUZkvTLJ4rJwF2HZBUehrm3pAoGBANg1+tveNCyRGbAuG/M0\nvgXbwO+FXWojWP1xrhT3gyMNbOm079FI20Ty3F6XRmfRtF7stRyN5udPGaz33jlJ\n/rBEsNybQiK8qyCNzZtQVYFG1C4SSI8GbO5Vk7cTSphhwDlsEKvJWuX+I36BWKts\nFPQwjI/ImIvmjdUKP1Y7XQ51AoGBALWa5Y3ASRvStCqkUlfFH4TuuWiTcM2VnN+b\nV4WrKnu/kKKWs+x09rpbzjcf5kptaGrvRp2sM+Yh0RhByCmt5fBF4OWXRJxy5lMO\nT78supJgpcbc5YvfsJvs9tHIYrPvtT0AyrI5B33od74wIhrCiz5YCQCAygVuCleY\ndpQXSp1RAoGBAKjasot7y/ErVxq7LIpGgoH+XTxjvMsj1JwlMeK0g3sjnun4g4oI\nPBtpER9QaSFi2OeYPklJ2g2yvFcVzj/pFk/n1Zd9pWnbU+JIXBYaHTjmktLeZHsb\nrTEKATo+Y1Alrhpr/z7gXXDfuKKXHkVRiper1YRAxELoLJB8r7LWeuIb\n-----END RSA PRIVATE KEY-----" diff --git a/guides/api.md b/guides/api.md new file mode 100644 index 000000000..f8e7f1502 --- /dev/null +++ b/guides/api.md @@ -0,0 +1,40 @@ +# Hubs Server API +Reticulum includes a [GraphQL](https://graphql.org/) API that grants programmatic access to server resources. + +Note: This API is currently in alpha testing and is not yet available for use. (Users cannot generate API Access Tokens.) + +## Accessing the API +Hubs Cloud administrators can enable or disable the API by toggling `App Settings > Features > Public API Access` in the admin panel. + +Once enabled, the API can be accessed by sending HTTP `GET` and `POST` requests to `/api/v2_alpha/`. + +## Authentication and Authorization +You must attach an API Access Token with each request. + +To attach an API Access Token to a request, add an `HTTP` header named `Authorization` with value `Bearer: `. + +### API Access Token Types +There are two types of API Access Tokens: +- `:account` tokens act on behalf of a specific user +- `:app` tokens act on behalf of the Hubs Cloud itself + +### Scopes +Each API Access Token specifies its `scopes`. Scopes allow a token to be used to perform specific actions. + +| Scope | API Actions | +| --: | --- | +| `read_rooms` | `myRooms`, `favoriteRooms`, `publicRooms` | +| `write_rooms` | `createRoom`, `updateRoom` | + +Scopes, actions, and token types are expected to expand over time. + +## Examples +Reticulum ships with [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#graphiql), a graphical, interactive, in-browser GraphQL IDE that makes it easier to test and learn the API. It can be accessed by navigating to `/api/v2_alpha/graphiql`. + +[This example workspace](../test/api/v2/graphiql-workspace-2020-12-07-15-26-56.json) demonstrates several queries and can be loaded into the GraphiQL interface. You will have to supply your own API access token(s). + +Requests can also be sent by +- an `HTTP` client library, +- a command line tool like `curl`, +- a GraphQL-specific client library, or +- any other tool that speaks `HTTP`. diff --git a/habitat/default.toml b/habitat/default.toml index ee5bacd37..d5ae8886a 100644 --- a/habitat/default.toml +++ b/habitat/default.toml @@ -4,6 +4,7 @@ pool = "default" [phx] port = 4000 ip = "127.0.0.1" +# This config value is for local development only. secret_key = "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+7GHLKqt0gR9qzgThxb5" url_host_prefix = "" static_url_host_prefix = "" @@ -12,9 +13,11 @@ secondary_url_host = "" allow_crawlers = true [guardian] +# This config value is for local development only. secret_key = "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+7GHLKqt0gR9qzgThxb5" [erlang] +# This config value is for local development only. node_cookie = "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+7GHLKqt0gR9qzgThxb5" [habitat] diff --git a/lib/mix/tasks/generate_api_token.ex b/lib/mix/tasks/generate_api_token.ex new file mode 100644 index 000000000..f58dd8b51 --- /dev/null +++ b/lib/mix/tasks/generate_api_token.ex @@ -0,0 +1,78 @@ +defmodule Mix.Tasks.GenerateApiToken do + @moduledoc "Generates an Api Token for the given account email" + + use Mix.Task + + alias Ret.{Account} + alias Ret.Api.TokenUtils + + @impl Mix.Task + def run(_) do + user_or_app = + "Generate user token or app token? [user or app]" + |> Mix.shell().prompt() + |> String.trim() + + case user_or_app do + "user" -> + gen_user_token() + + "app" -> + gen_app_token() + + _ -> + Mix.shell().error("Input not recognized. Type \"user\" or \"app\".") + run([]) + end + end + + defp gen_user_token() do + email = + "Enter email address of the user whose account will be associated in this token: [foo@bar.com]\n" + |> Mix.shell().prompt() + |> String.trim() + + Mix.Task.run("app.start") + + case Account.account_for_email(email) do + nil -> + Mix.shell().error("Could not find account for the given email address: #{email}") + + account -> + IO.puts("Account found:") + + account + |> Inspect.Algebra.to_doc(%Inspect.Opts{}) + |> Inspect.Algebra.format(80) + |> IO.puts() + + if Mix.shell().yes?("Generate token for this account [#{email}]?") do + gen_token_for_account(account) + end + end + end + + defp gen_app_token() do + if Mix.shell().yes?("Are you sure you want to generate an app token?") do + Mix.Task.run("app.start") + + case TokenUtils.gen_app_token() do + {:ok, token, _claims} -> + Mix.shell().info("Successfully generated token:\n#{token}") + + {:error, reason} -> + Mix.shell().error("Error: #{reason}") + end + end + end + + defp gen_token_for_account(account) do + case TokenUtils.gen_token_for_account(account) do + {:ok, token, _claims} -> + Mix.shell().info("Successfully generated token:\n#{token}") + + {:error, reason} -> + Mix.shell().error("Error: #{reason}") + end + end +end diff --git a/lib/ret/account.ex b/lib/ret/account.ex index 3a052c0a1..d56f20076 100644 --- a/lib/ret/account.ex +++ b/lib/ret/account.ex @@ -24,6 +24,14 @@ defmodule Ret.Account do timestamps() end + def query do + from(account in Account) + end + + def where_account_id_is(query, id) do + from(account in query, where: account.account_id == ^id) + end + def has_accounts?(), do: from(a in Account, limit: 1) |> Repo.exists?() def has_admin_accounts?(), do: from(a in Account, limit: 1) |> where(is_admin: true) |> Repo.exists?() def exists_for_email?(email), do: account_for_email(email) != nil diff --git a/lib/ret/api/can_credentials.ex b/lib/ret/api/can_credentials.ex new file mode 100644 index 000000000..4d003d757 --- /dev/null +++ b/lib/ret/api/can_credentials.ex @@ -0,0 +1,103 @@ +defimpl Canada.Can, for: Ret.Api.Credentials do + import Canada, only: [can?: 2] + alias Ret.{Account, Hub} + alias Ret.Api.{Credentials, Scopes} + + def can?( + %Credentials{is_revoked: true}, + _action, + _resource + ) do + false + end + + def can?( + %Credentials{subject_type: :app, scopes: scopes}, + :get_rooms_created_by, + %Account{} = account + ) do + Scopes.read_rooms() in scopes and can?(:reticulum_app_token, get_rooms_created_by(account)) + end + + def can?( + %Credentials{subject_type: :account, account: subject, scopes: scopes}, + :get_rooms_created_by, + %Account{} = account + ) do + Scopes.read_rooms() in scopes and can?(subject, get_rooms_created_by(account)) + end + + def can?( + %Credentials{subject_type: :app, scopes: scopes}, + :get_favorite_rooms_of, + %Account{} = account + ) do + Scopes.read_rooms() in scopes and can?(:reticulum_app_token, get_favorite_rooms_of(account)) + end + + def can?( + %Credentials{subject_type: :account, account: subject, scopes: scopes}, + :get_favorite_rooms_of, + %Account{} = account + ) do + Scopes.read_rooms() in scopes and can?(subject, get_favorite_rooms_of(account)) + end + + def can?( + %Credentials{subject_type: :app, scopes: scopes}, + :get_public_rooms, + _ + ) do + Scopes.read_rooms() in scopes and can?(:reticulum_app_token, get_public_rooms(nil)) + end + + def can?( + %Credentials{subject_type: :account, account: subject, scopes: scopes}, + :get_public_rooms, + _ + ) do + Scopes.read_rooms() in scopes and can?(subject, get_public_rooms(nil)) + end + + def can?( + %Credentials{subject_type: :app, scopes: scopes}, + :create_room, + _ + ) do + Scopes.write_rooms() in scopes && can?(:reticulum_app_token, create_hub(nil)) + end + + def can?( + %Credentials{subject_type: :account, account: subject, scopes: scopes}, + :create_room, + _ + ) do + Scopes.write_rooms() in scopes && can?(subject, create_hub(nil)) + end + + def can?(%Credentials{subject_type: :app, scopes: scopes}, :embed_hub, %Hub{} = hub) do + Scopes.read_rooms() in scopes && can?(:reticulum_app_token, embed_hub(hub)) + end + + def can?(%Credentials{subject_type: :account, account: subject, scopes: scopes}, :embed_hub, %Hub{} = hub) do + Scopes.read_rooms() in scopes && can?(subject, embed_hub(hub)) + end + + def can?( + %Credentials{subject_type: :app, scopes: scopes}, + :update_room, + %Hub{} = hub + ) do + Scopes.write_rooms() in scopes && can?(:reticulum_app_token, update_hub(hub)) + end + + def can?( + %Credentials{subject_type: :account, account: subject, scopes: scopes}, + :update_room, + %Hub{} = hub + ) do + Scopes.write_rooms() in scopes && can?(subject, update_hub(hub)) + end + + def can?(_, _, _), do: false +end diff --git a/lib/ret/api/credentials.ex b/lib/ret/api/credentials.ex new file mode 100644 index 000000000..b6b4c7cc4 --- /dev/null +++ b/lib/ret/api/credentials.ex @@ -0,0 +1,128 @@ +defmodule Ret.Api.Credentials do + @moduledoc """ + Credentials for API access. + """ + alias Ret.Api.Credentials + + alias Ret.Account + + use Ecto.Schema + import Ecto.Query, only: [from: 2] + import Ecto.Changeset + alias Ret.Api.{TokenSubjectType, ScopeType} + + @schema_prefix "ret0" + @primary_key {:api_credentials_id, :id, autogenerate: true} + + schema "api_credentials" do + field(:api_credentials_sid, :string) + field(:token_hash, :string) + field(:subject_type, TokenSubjectType) + field(:is_revoked, :boolean) + field(:scopes, {:array, ScopeType}) + + belongs_to(:account, Account, references: :account_id) + timestamps() + end + + @required_keys [:api_credentials_sid, :token_hash, :subject_type, :is_revoked, :scopes] + @permitted_keys @required_keys + + def generate_credentials(%{subject_type: _st, scopes: _sc, account_or_nil: account_or_nil} = params) do + sid = Ret.Sids.generate_sid() + + # Use 18 bytes (not 16, the default) to avoid having all tokens end in "09" + # See https://github.com/patricksrobertson/secure_random.ex/issues/11 + # Prefix the sid to the rest of the token for ease of management + token = "#{sid}.#{SecureRandom.urlsafe_base64(18)}" + + params = + Map.merge(params, %{ + api_credentials_sid: sid, + token_hash: Ret.Crypto.hash(token), + is_revoked: false + }) + + case %Credentials{} + |> change() + |> cast(params, @permitted_keys) + |> maybe_put_assoc_account(account_or_nil) + |> validate_required(@required_keys) + |> validate_change(:subject_type, &validate_field/2) + |> validate_change(:scopes, &validate_field/2) + |> unique_constraint(:api_credentials_sid) + |> unique_constraint(:token_hash) + # TODO: We can pass multiple fields to unique_contraint when we update ecto + # https://github.com/elixir-ecto/ecto/pull/3276 + |> Ret.Repo.insert() do + {:ok, credentials} -> + {:ok, token, credentials} + + {:error, reason} -> + {:error, reason} + end + end + + defp maybe_put_assoc_account(changeset, %Account{} = account) do + put_assoc(changeset, :account, account) + end + + defp maybe_put_assoc_account(changeset, nil) do + changeset + end + + defp validate_single_scope_type(scope) do + if ScopeType.valid_value?(scope) do + [] + else + [invalid_scope: "Unrecognized scope type. Got #{scope}."] + end + end + + def validate_field(:scopes, scopes) do + Enum.reduce(scopes, [], fn scope, errors -> + errors ++ validate_single_scope_type(scope) + end) + end + + def validate_field(:subject_type, subject_type) do + if TokenSubjectType.valid_value?(subject_type) do + [] + else + [invalid_subject_type: "Unrecognized subject type. Must be app or account. Got #{subject_type}."] + end + end + + def revoke(credentials) do + credentials + |> change() + |> put_change(:is_revoked, true) + |> Ret.Repo.update() + end + + def query do + from(c in Credentials, left_join: a in Account, on: c.account_id == a.account_id, preload: [account: a]) + end + + def where_sid_is(query, sid) do + from([credential, _account] in query, + where: credential.api_credentials_sid == ^sid + ) + end + + def where_token_hash_is(query, hash) do + from([credential, _account] in query, + where: credential.token_hash == ^hash + ) + end + + def where_account_is(query, %Account{account_id: id}) do + from([credential, _account] in query, + where: credential.account_id == ^id + ) + end + + def app_token_query() do + from(c in Credentials, where: c.subject_type == ^:app) + end +end diff --git a/lib/ret/api/dataloader.ex b/lib/ret/api/dataloader.ex new file mode 100644 index 000000000..cc73be3bf --- /dev/null +++ b/lib/ret/api/dataloader.ex @@ -0,0 +1,11 @@ +defmodule Ret.Api.Dataloader do + @moduledoc "Configuration for dataloader" + + import Ecto.Query + alias Ret.{Repo, Scene, SceneListing} + + def source(), do: Dataloader.Ecto.new(Repo, query: &query/2) + # Guard against loading removed scenes or delisted scene listings + def query(Scene, _), do: from(s in Scene, where: s.state != ^:removed) + def query(SceneListing, _), do: from(sl in SceneListing, where: sl.state != ^:delisted) +end diff --git a/lib/ret/api/rooms.ex b/lib/ret/api/rooms.ex new file mode 100644 index 000000000..b13cdd13c --- /dev/null +++ b/lib/ret/api/rooms.ex @@ -0,0 +1,107 @@ +defmodule Ret.Api.Rooms do + @moduledoc "Functions for accessing rooms in an authenticated way" + + alias Ret.{Account, Hub, Repo} + alias RetWeb.Api.V1.HubView + alias Ret.Api.{Credentials} + + import Canada, only: [can?: 2] + + def authed_get_embed_token(%Credentials{} = credentials, hub) do + if can?(credentials, embed_hub(hub)) do + {:ok, hub.embed_token} + else + {:ok, nil} + end + end + + def authed_get_rooms_created_by(%Account{} = account, %Credentials{} = credentials, params) do + if can?(credentials, get_rooms_created_by(account)) do + {:ok, Hub.get_my_rooms(account, params)} + else + {:error, :invalid_credentials} + end + end + + def authed_get_favorite_rooms_of(%Account{} = account, %Credentials{} = credentials, params) do + if can?(credentials, get_favorite_rooms_of(account)) do + {:ok, Hub.get_favorite_rooms(account, params)} + else + {:error, :invalid_credentials} + end + end + + def authed_get_public_rooms(%Credentials{} = credentials, params) do + if can?(credentials, get_public_rooms(nil)) do + {:ok, Hub.get_public_rooms(params)} + else + {:error, :invalid_credentials} + end + end + + def authed_create_room(%Credentials{} = credentials, params) do + if can?(credentials, create_room(nil)) do + Hub.create_room(params, credentials.account) + else + {:error, :invalid_credentials} + end + end + + def authed_update_room(hub_sid, %Credentials{} = credentials, params) do + hub = Hub |> Repo.get_by(hub_sid: hub_sid) |> Repo.preload([:hub_role_memberships, :hub_bindings]) + + if is_nil(hub) do + {:error, "Cannot find room with id: " <> hub_sid} + else + if can?(credentials, update_room(hub)) do + update_room(hub, credentials, params) + else + {:error, :invalid_credentials} + end + end + end + + defp update_room(hub, %Credentials{} = credentials, params) do + hub + |> Repo.preload(Hub.hub_preloads()) + |> Hub.add_attrs_to_changeset(params) + |> Hub.maybe_add_member_permissions(hub, params) + |> Hub.add_scene_changes_to_changeset(params) + |> try_do_update_room(credentials) + end + + defp subject_for(%Credentials{subject_type: :app}), do: :reticulum_app_token + defp subject_for(%Credentials{subject_type: :account, account: account}), do: account + + defp try_do_update_room({:error, reason}, _) do + {:error, reason} + end + + defp try_do_update_room(changeset, %Credentials{} = c) do + subject = subject_for(c) + + case changeset |> Repo.update() do + {:error, changeset} -> + {:error, changeset} + + {:ok, hub} -> + hub = Repo.preload(hub, Hub.hub_preloads()) + + case broadcast_hub_refresh(hub, subject, Map.keys(changeset.changes) |> Enum.map(&Atom.to_string(&1))) do + {:error, reason} -> {:error, reason} + :ok -> {:ok, hub} + end + end + end + + defp broadcast_hub_refresh(hub, subject, stale_fields) do + payload = + HubView.render("show.json", %{ + hub: hub, + embeddable: subject |> can?(embed_hub(hub)) + }) + |> Map.put(:stale_fields, stale_fields) + + RetWeb.Endpoint.broadcast("hub:" <> hub.hub_sid, "hub_refresh", payload) + end +end diff --git a/lib/ret/api/scopes.ex b/lib/ret/api/scopes.ex new file mode 100644 index 000000000..f3739a730 --- /dev/null +++ b/lib/ret/api/scopes.ex @@ -0,0 +1,11 @@ +defmodule Ret.Api.Scopes do + @moduledoc false + def read_rooms, do: :read_rooms + def write_rooms, do: :write_rooms + + def all_scopes, + do: [ + read_rooms(), + write_rooms() + ] +end diff --git a/lib/ret/api/token.ex b/lib/ret/api/token.ex new file mode 100644 index 000000000..4c2d71d25 --- /dev/null +++ b/lib/ret/api/token.ex @@ -0,0 +1,18 @@ +defmodule Ret.Api.Token do + @moduledoc """ + ApiTokens determine what actions are allowed to be taken via the public API. + """ + use Guardian, token_module: Ret.Api.TokenModule, otp_app: :ret + + alias Ret.Api.Credentials + + def subject_for_token(_, _), do: {:ok, nil} + + def resource_from_claims(%Credentials{} = credentials) do + {:ok, credentials} + end + + def resource_from_claims(_) do + {:error, :invalid_token} + end +end diff --git a/lib/ret/api/token_module.ex b/lib/ret/api/token_module.ex new file mode 100644 index 000000000..ecb5cb06a --- /dev/null +++ b/lib/ret/api/token_module.ex @@ -0,0 +1,109 @@ +defmodule Ret.Api.TokenModule do + @moduledoc """ + This module should not be used directly. + + It is intended to be used by Guardian. + """ + alias Ret.{Account, Repo} + alias Ret.Api.Credentials + @behaviour Guardian.Token + + @doc """ + No concept of validating signature so we just decode the token + """ + def peek(mod, token) do + case decode_token(mod, token) do + {:ok, %Credentials{} = credentials} -> %{claims: credentials} + {:ok, {:error, _reason}} -> nil + _ -> nil + end + end + + @doc """ + Do not need to generate a token_id here + """ + def token_id, do: nil + + @doc """ + Builds the default claims for API tokens. + """ + def build_claims(_mod, _resource, _sub, claims \\ %{}, _options \\ []) do + {:ok, claims} + end + + defp ensure_atom(x) when is_atom(x), do: x + defp ensure_atom(x) when is_binary(x), do: String.to_atom(x) + + defp get_account(id) when is_nil(id) do + {:ok, nil} + end + + defp get_account(id) do + case Account.query() + |> Account.where_account_id_is(id) + |> Repo.one() do + nil -> {:error, "Could not find account"} + account -> {:ok, account} + end + end + + @doc """ + Create a token. + """ + def create_token(_mod, claims, _options \\ []) do + account_id = Map.get(claims, "account_id", nil) + + case get_account(account_id) do + {:ok, account_or_nil} -> + case Ret.Api.Credentials.generate_credentials(%{ + subject_type: ensure_atom(Map.get(claims, "subject_type")), + scopes: Map.get(claims, "scopes"), + account_or_nil: account_or_nil + }) do + {:ok, token, _credentials} -> {:ok, token} + _ -> {:error, "Failed to create token for claims."} + end + + {:error, _reason} -> + {:error, "Failed to create token for claims."} + end + end + + @doc """ + Decodes the token and validates the signature. + """ + def decode_token(_mod, token, _options \\ []) do + case Credentials.query() + |> Credentials.where_token_hash_is(Ret.Crypto.hash(token)) + |> Ret.Repo.one() do + # Don't want to return the error at this level, + # so we pass it along for graphql to handle + nil -> {:ok, {:error, :invalid_token}} + credentials -> {:ok, credentials} + end + end + + @doc """ + Verifies the claims. + """ + def verify_claims(_mod, claims, _options) do + {:ok, claims} + end + + @doc """ + Revoke a token + """ + def revoke(_mod, %Credentials{} = credentials, _token, _options) do + Ret.Api.Credentials.revoke(credentials) + end + + @doc """ + Refresh the token + """ + def refresh(_mod, _old_token, _options), do: nil + + @doc """ + Exchange a token of one type to another. + """ + def exchange(_mod, _old_token, _from_type, _to_type, _options), do: nil +end diff --git a/lib/ret/api/token_utils.ex b/lib/ret/api/token_utils.ex new file mode 100644 index 000000000..15d725797 --- /dev/null +++ b/lib/ret/api/token_utils.ex @@ -0,0 +1,115 @@ +defmodule Ret.Api.TokenUtils do + @moduledoc """ + Utility functions for generating API access tokens. + """ + alias Ret.{Account, Repo} + alias Ret.Api.{Credentials, Token, Scopes} + + import Canada, only: [can?: 2] + + def gen_app_token(scopes \\ [Scopes.read_rooms(), Scopes.write_rooms()]) do + Token.encode_and_sign(nil, %{ + subject_type: :app, + scopes: scopes, + account_id: nil + }) + end + + def gen_token_for_account(%Account{} = account, scopes \\ [Scopes.read_rooms(), Scopes.write_rooms()]) do + Token.encode_and_sign(nil, %{ + subject_type: :account, + scopes: scopes, + account_id: account.account_id + }) + end + + defp ensure_atom(x) when is_atom(x), do: x + defp ensure_atom(x) when is_binary(x), do: String.to_atom(x) + + defp account_id_from_args(%Account{}, %{"account_id" => account_id}) do + # This account wants to create credentials on behalf of another account + account_id + end + + defp account_id_from_args(%Account{} = account, _params) do + # This account wants to create credentials for itself + account.account_id + end + + defp validate_account_id(%Account{account_id: account_id}, account_id) do + # Skip a trip to the DB if account is creating credentials for itself + [] + end + + defp validate_account_id(%Account{}, account_id) do + # Make sure the given account_id refers to an account that actually exists + validate_field(:account_id, account_id) + end + + defp validate_field(:account_id, account_id) do + if Account.query() + |> Account.where_account_id_is(account_id) + |> Repo.exists?() do + [] + else + [account_id: "Invalid account id #{account_id}"] + end + end + + def to_claims(%Account{} = account, %{"subject_type" => subject_type, "scopes" => scopes} = params) do + account_id_for_claims = account_id_from_args(account, params) + + case validate_account_id(account, account_id_for_claims) ++ + Credentials.validate_field(:scopes, scopes) ++ + Credentials.validate_field(:subject_type, subject_type) do + [] -> + # It is safe to cast user-provided strings to atoms here + # because we validated that the strings match our atoms + # (with &valid_value?/1 from ecto_enum) + {:ok, + %{ + account_id: account_id_for_claims, + subject_type: ensure_atom(subject_type), + scopes: Enum.map(scopes, &ensure_atom/1) + }} + + error_list -> + {:error, error_list} + end + end + + def authed_create_credentials(account, claims) do + if can?(account, create_credentials(claims)) do + Token.encode_and_sign(nil, claims) + else + {:error, :unauthorized} + end + end + + def authed_list_credentials(account, subject_type) do + if can?(account, list_credentials(subject_type)) do + list_credentials(account, subject_type) + else + {:error, :unauthorized} + end + end + + defp list_credentials(account, :account) do + Credentials.query() + |> Credentials.where_account_is(account) + |> Repo.all() + end + + defp list_credentials(_account, :app) do + Credentials.app_token_query() + |> Repo.all() + end + + def authed_revoke_credentials(account, credentials) do + if can?(account, revoke_credentials(credentials)) do + Credentials.revoke(credentials) + else + {:error, :unauthorized} + end + end +end diff --git a/lib/ret/cached_file.ex b/lib/ret/cached_file.ex index ca9111a34..46f1e986d 100644 --- a/lib/ret/cached_file.ex +++ b/lib/ret/cached_file.ex @@ -77,9 +77,9 @@ defmodule Ret.CachedFile do def vacuum do Ret.Locking.exec_if_lockable(:cached_file_vacuum, fn -> # Underlying files will be removed by storage vacuum - one_day_ago = Timex.now() |> Timex.shift(days: -1) + two_days_ago = Timex.now() |> Timex.shift(days: -2) - from(f in CachedFile, where: f.inserted_at() < ^one_day_ago) + from(f in CachedFile, where: f.inserted_at() < ^two_days_ago) |> Repo.delete_all() end) end diff --git a/lib/ret/enums.ex b/lib/ret/enums.ex index 89cd8614a..580b1bebb 100644 --- a/lib/ret/enums.ex +++ b/lib/ret/enums.ex @@ -11,3 +11,5 @@ defenum(Ret.Avatar.State, :avatar_state, [:active, :removed], schema: "ret0") defenum(Ret.AvatarListing.State, :avatar_listing_state, [:active, :delisted, :removed], schema: "ret0") defenum(Ret.Account.State, :account_state, [:enabled, :disabled], schema: "ret0") defenum(Ret.Asset.Type, :asset_type, [:image, :video, :model, :audio], schema: "ret0") +defenum(Ret.Api.TokenSubjectType, :api_token_subject_type, [:app, :account], schema: "ret0") +defenum(Ret.Api.ScopeType, :api_scope_type, Ret.Api.Scopes.all_scopes(), schema: "ret0") diff --git a/lib/ret/http_utils.ex b/lib/ret/http_utils.ex index e72287c6b..e58aca264 100644 --- a/lib/ret/http_utils.ex +++ b/lib/ret/http_utils.ex @@ -24,6 +24,9 @@ defmodule Ret.HttpUtils do end def retry_until_success(verb, url, body \\ "", headers \\ [], cap_ms \\ 5_000, expiry_ms \\ 10_000) do + headers = + headers ++ [{"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"}] + hackney_options = if module_config(:insecure_ssl) == true do [:insecure] diff --git a/lib/ret/hub.ex b/lib/ret/hub.ex index 52cd745c1..a8a4a45dd 100644 --- a/lib/ret/hub.ex +++ b/lib/ret/hub.ex @@ -24,7 +24,8 @@ defmodule Ret.Hub do RoomAssigner, BitFieldUtils, HubRoleMembership, - AppConfig + AppConfig, + AccountFavorite } alias Ret.Hub.{HubSlug} @@ -100,8 +101,8 @@ defmodule Ret.Hub do field(:spawned_object_types, :integer, default: 0) field(:entry_mode, Ret.Hub.EntryMode) field(:user_data, :map) - belongs_to(:scene, Ret.Scene, references: :scene_id) - belongs_to(:scene_listing, Ret.SceneListing, references: :scene_listing_id) + belongs_to(:scene, Ret.Scene, references: :scene_id, on_replace: :nilify) + belongs_to(:scene_listing, Ret.SceneListing, references: :scene_listing_id, on_replace: :nilify) has_many(:web_push_subscriptions, Ret.WebPushSubscription, foreign_key: :hub_id) belongs_to(:created_by_account, Ret.Account, references: :account_id) has_many(:hub_invites, Ret.HubInvite, foreign_key: :hub_id) @@ -115,6 +116,201 @@ defmodule Ret.Hub do timestamps() end + @required_keys [ + :name, + :hub_sid, + :host, + :entry_code, + :entry_code_expires_at, + :embed_token, + :member_permissions, + :max_occupant_count, + :spawned_object_types, + :room_size + ] + @permitted_keys [ + :creator_assignment_token, + :description, + :embedded, + :default_environment_gltf_bundle_url, + :user_data, + :last_active_at, + :entry_mode | @required_keys + ] + + # TODO: This function was created for use in the public API. + # It would be good to revisit this and the alternatives below + # so that there did not need to be as many variations. + def create_room(params, account_or_nil) do + with {:ok, params} <- parse_member_permissions(params) do + params = + Map.merge( + %{ + name: Ret.RandomRoomNames.generate_room_name(), + hub_sid: Ret.Sids.generate_sid(), + host: RoomAssigner.get_available_host(nil), + entry_code: generate_entry_code!(), + entry_code_expires_at: + Timex.now() + |> Timex.shift(hours: @entry_code_expiration_hours) + |> DateTime.truncate(:second), + creator_assignment_token: SecureRandom.hex(), + embed_token: SecureRandom.hex(), + member_permissions: default_member_permissions(), + room_size: AppConfig.get_cached_config_value("features|default_room_size") + }, + params + ) + + result = + %Hub{} + |> change() + |> cast(params, @permitted_keys) + |> add_account_to_changeset(account_or_nil) + |> add_scene_changes_to_changeset(params) + |> HubSlug.maybe_generate_slug() + |> validate_required(@required_keys) + |> validate_length(:name, max: 64) + |> validate_length(:description, max: 64_000) + |> validate_number(:room_size, + greater_than_or_equal_to: 0, + less_than_or_equal_to: AppConfig.get_cached_config_value("features|max_room_size") + ) + |> unique_constraint(:hub_sid) + |> unique_constraint(:entry_code) + |> Repo.insert() + + case result do + {:ok, hub} -> + {:ok, Repo.preload(hub, hub_preloads())} + + _ -> + result + end + end + end + + # TODO: Clean up handling of member_permissions so that it is + # clear everywhere whether we are dealing with a map or an int + defp parse_member_permissions(%{member_permissions: map} = params) when is_map(map) do + case Hub.lenient_member_permissions_to_int(map) do + {:ok, member_permissions} -> + {:ok, %{params | member_permissions: member_permissions}} + + {ArgumentError, e} -> + {:error, e} + end + end + + defp parse_member_permissions(%{member_permissions: nil} = params) do + {:ok, Map.delete(params, :member_permissions)} + end + + defp parse_member_permissions(params) do + {:ok, params} + end + + defp default_member_permissions() do + if Ret.AppConfig.get_config_bool("features|permissive_rooms") do + member_permissions_to_int(@default_member_permissions) + else + member_permissions_to_int(@default_restrictive_member_permissions) + end + end + + def add_scene_changes_to_changeset(changeset, %{} = params) do + add_scene_changes(changeset, scene_change_from_params(params)) + end + + defp scene_change_from_params(%{scene_id: nil, scene_url: nil}) do + :nilify + end + + defp scene_change_from_params(%{scene_id: _id, scene_url: _url}) do + {:error, + %{key: :scene_id, message: "Cannot specify both scene_id and scene_url. Choose one or the other (or neither)."}} + end + + defp scene_change_from_params(%{scene_url: nil}) do + :nilify + end + + defp scene_change_from_params(%{scene_id: nil}) do + :nilify + end + + defp scene_change_from_params(%{scene_url: url}) do + endpoint_host = RetWeb.Endpoint.host() + + case url |> URI.parse() do + %URI{host: ^endpoint_host, path: "/scenes/" <> scene_path} -> + scene_or_scene_listing = scene_path |> String.split("/") |> Enum.at(0) |> Scene.scene_or_scene_listing_by_sid() + + if is_nil(scene_or_scene_listing) do + {:error, %{key: :scene_url, message: "Cannot find scene with url: " <> url}} + else + scene_or_scene_listing + end + + _ -> + url + end + end + + defp scene_change_from_params(%{scene_id: id}) do + scene_or_scene_listing = Scene.scene_or_scene_listing_by_sid(id) + + if is_nil(scene_or_scene_listing) do + {:error, %{key: :scene_id, message: "Cannot find scene with id: " <> id}} + else + scene_or_scene_listing + end + end + + defp scene_change_from_params(_params) do + nil + end + + defp add_scene_changes(changeset, {:error, %{key: key, message: message}}) do + add_error(changeset, key, message) + end + + defp add_scene_changes(changeset, nil) do + # No scene info in params. Leave unchanged + changeset + end + + defp add_scene_changes(changeset, :nilify) do + # Clear scene info + changeset + |> put_change(:default_environment_gltf_bundle_url, nil) + |> put_assoc(:scene, nil) + |> put_assoc(:scene_listing, nil) + end + + defp add_scene_changes(changeset, %Scene{} = scene) do + changeset + |> put_assoc(:scene, scene) + |> put_assoc(:scene_listing, nil) + |> put_change(:default_environment_gltf_bundle_url, nil) + end + + defp add_scene_changes(changeset, %SceneListing{} = scene_listing) do + changeset + |> put_assoc(:scene, nil) + |> put_assoc(:scene_listing, scene_listing) + |> put_change(:default_environment_gltf_bundle_url, nil) + end + + defp add_scene_changes(changeset, url) do + changeset + |> cast(%{default_environment_gltf_bundle_url: url}, [:default_environment_gltf_bundle_url]) + # TODO: Should we validate the format of the URL? + |> validate_required([:default_environment_gltf_bundle_url]) + |> put_assoc(:scene, nil) + |> put_assoc(:scene_listing, nil) + end + # Create new room, inserts into db # returns newly created %Hub def create_new_room(%{"name" => _name} = params, true = _add_to_db) do @@ -134,6 +330,14 @@ defmodule Ret.Hub do |> changeset(scene_or_scene_listing, params) end + def create(params) do + scene_or_scene_listing = get_scene_or_scene_listing(params) + + %Hub{} + |> changeset(scene_or_scene_listing, params) + |> Repo.insert() + end + defp get_scene_or_scene_listing(params) do if is_nil(params["scene_id"]) do SceneListing.get_random_default_scene_listing() @@ -142,6 +346,14 @@ defmodule Ret.Hub do end end + defp get_scene_or_scene_listing_by_id(nil) do + SceneListing.get_random_default_scene_listing() + end + + defp get_scene_or_scene_listing_by_id(id) do + Scene.scene_or_scene_listing_by_sid(id) + end + def get_by_entry_code_string(entry_code_string) when is_binary(entry_code_string) do case Integer.parse(entry_code_string) do {entry_code, _} -> Hub |> Repo.get_by(entry_code: entry_code) @@ -149,6 +361,31 @@ defmodule Ret.Hub do end end + def get_my_rooms(account, params) do + Hub + |> where([h], h.created_by_account_id == ^account.account_id and h.entry_mode in ^["allow", "invite"]) + |> order_by(desc: :inserted_at) + |> preload(^Hub.hub_preloads()) + |> Repo.paginate(params) + end + + def get_favorite_rooms(account, params) do + Hub + |> where([h], h.entry_mode in ^["allow", "invite"]) + |> join(:inner, [h], f in AccountFavorite, on: f.hub_id == h.hub_id and f.account_id == ^account.account_id) + |> order_by([h, f], desc: f.last_activated_at) + |> preload(^Hub.hub_preloads()) + |> Repo.paginate(params) + end + + def get_public_rooms(params) do + Hub + |> where([h], h.allow_promotion and h.entry_mode in ^["allow", "invite"]) + |> order_by(desc: :inserted_at) + |> preload(^Hub.hub_preloads()) + |> Repo.paginate(params) + end + def changeset(%Hub{} = hub, %Scene{} = scene, attrs) do hub |> changeset(nil, attrs) @@ -190,9 +427,9 @@ defmodule Ret.Hub do attrs["member_permissions"] |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) |> member_permissions_to_int end - def add_member_permissions_update_to_changeset(changeset, hub, attrs) do + defp add_member_permissions_update_to_changeset(changeset, hub, member_permissions) do member_permissions = - Map.merge(member_permissions_for_hub(hub), attrs["member_permissions"]) + Map.merge(member_permissions_for_hub(hub), member_permissions) |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) |> member_permissions_to_int @@ -213,7 +450,8 @@ defmodule Ret.Hub do end def add_promotion_to_changeset(changeset, attrs) do - changeset |> put_change(:allow_promotion, !!attrs["allow_promotion"]) + changeset + |> put_change(:allow_promotion, Map.get(attrs, "allow_promotion", false) || Map.get(attrs, :allow_promotion, false)) end def maybe_add_entry_mode_to_changeset(changeset, attrs) do @@ -224,6 +462,20 @@ defmodule Ret.Hub do end end + def maybe_add_new_scene_to_changeset(changeset, %{scene_id: scene_id}) do + scene_or_scene_listing = get_scene_or_scene_listing_by_id(scene_id) + + if is_nil(scene_or_scene_listing) do + {:error, "Cannot find scene with id " <> scene_id} + else + Hub.add_new_scene_to_changeset(changeset, scene_or_scene_listing) + end + end + + def maybe_add_new_scene_to_changeset(changeset, _args) do + changeset + end + def changeset_for_new_seen_occupant_count(%Hub{} = hub, occupant_count) do new_max_occupant_count = max(hub.max_occupant_count, occupant_count) @@ -358,6 +610,15 @@ defmodule Ret.Hub do hub.room_size || AppConfig.get_cached_config_value("features|default_room_size") end + def scene_or_scene_listing_for(%Hub{} = hub) do + case hub.scene || hub.scene_listing do + nil -> nil + %Scene{state: :removed} -> nil + %SceneListing{state: :delisted} -> nil + scene_or_scene_listing -> scene_or_scene_listing + end + end + defp changeset_for_new_entry_code(%Hub{} = hub) do hub |> Ecto.Changeset.change() @@ -524,17 +785,35 @@ defmodule Ret.Hub do is_creator?(hub, account_id) || hub_role_memberships |> Enum.any?(&(&1.account_id === account_id)) end - def member_permissions_to_int(%{} = member_permissions) do + @doc """ + Lenient version of member permissions conversion + Does not throw on invalid permissions + """ + def lenient_member_permissions_to_int(%{} = member_permissions) do invalid_member_permissions = member_permissions |> Map.drop(@member_permissions_keys) |> Map.keys() if invalid_member_permissions |> Enum.count() > 0 do - raise ArgumentError, "Invalid permissions #{invalid_member_permissions |> Enum.join(", ")}" + {ArgumentError, "Invalid permissions #{invalid_member_permissions |> Enum.join(", ")}"} + else + {:ok, + @member_permissions + |> Enum.reduce(0, fn {val, member_permission}, acc -> + if(member_permissions[member_permission], do: val, else: 0) + acc + end)} end + end - @member_permissions - |> Enum.reduce(0, fn {val, member_permission}, acc -> - if(member_permissions[member_permission], do: val, else: 0) + acc - end) + # TODO: Rename (lenient_)member_permissions_to_int + # to follow the elixir pattern of using an exclamation mark (!) + # to indicate possibly raising an error + def member_permissions_to_int(%{} = member_permissions) do + case lenient_member_permissions_to_int(member_permissions) do + {:ok, int} -> + int + + {ArgumentError, e} -> + raise ArgumentError, e + end end def has_member_permission?(%Hub{} = hub, member_permission) do @@ -551,6 +830,43 @@ defmodule Ret.Hub do |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end) end + def member_permissions_for_hub_as_atoms(%Hub{} = hub) do + hub.member_permissions + |> BitFieldUtils.permissions_to_map(@member_permissions) + end + + def maybe_add_member_permissions(changeset, hub, %{"member_permissions" => member_permissions}) do + add_member_permissions_update_to_changeset( + changeset, + hub, + member_permissions + ) + end + + def maybe_add_member_permissions(changeset, _hub, %{:member_permissions => nil}) do + changeset + end + + def maybe_add_member_permissions(changeset, hub, %{:member_permissions => member_permissions}) do + add_member_permissions_update_to_changeset( + changeset, + hub, + Map.new(member_permissions, fn {k, v} -> {Atom.to_string(k), v} end) + ) + end + + def maybe_add_member_permissions(changeset, _hub, _params) do + changeset + end + + def maybe_add_promotion(changeset, account, hub, %{"allow_promotion" => _} = hub_params), + do: changeset |> Hub.maybe_add_promotion_to_changeset(account, hub, hub_params) + + def maybe_add_promotion(changeset, account, hub, %{allow_promotion: _} = hub_params), + do: changeset |> Hub.maybe_add_promotion_to_changeset(account, hub, hub_params) + + def maybe_add_promotion(changeset, _account, _hub, _), do: changeset + # The account argument here can be a Ret.Account, a Ret.OAuthProvider or nil. def perms_for_account(%Ret.Hub{} = hub, account) do %{ @@ -580,6 +896,36 @@ end defimpl Canada.Can, for: Ret.Account do alias Ret.{Hub, AppConfig} + alias Ret.Api.Credentials + + def can?(%Ret.Account{is_admin: is_admin}, :create_credentials, _params) do + is_admin + end + + def can?(%Ret.Account{is_admin: is_admin}, :list_credentials, :app) do + is_admin + end + + def can?(%Ret.Account{}, :list_credentials, :account) do + # TODO: Allow admins to disable this in config + true + end + + def can?(%Ret.Account{}, :list_credentials, _subject_type) do + false + end + + def can?(%Ret.Account{account_id: account_id}, :revoke_credentials, %Credentials{account_id: account_id}) do + true + end + + def can?(%Ret.Account{is_admin: true}, :revoke_credentials, %Credentials{}) do + true + end + + def can?(%Ret.Account{}, :revoke_credentials, %Credentials{}) do + false + end @owner_actions [:update_hub, :close_hub, :embed_hub, :kick_users, :mute_users] @object_actions [:spawn_and_move_media, :spawn_camera, :spawn_drawing, :pin_objects, :spawn_emoji, :fly] @@ -653,6 +999,13 @@ defimpl Canada.Can, for: Ret.Account do hub |> Hub.has_member_permission?(action) or hub |> Ret.Hub.is_owner?(account_id) end + @self_allowed_actions [:get_rooms_created_by, :get_favorite_rooms_of] + # Allow accounts to access their own rooms + def can?(%Ret.Account{} = a, action, %Ret.Account{} = b) when action in @self_allowed_actions, + do: a.account_id == b.account_id + + def can?(%Ret.Account{}, :get_public_rooms, _), do: true + # Create hubs def can?(%Ret.Account{is_admin: true}, :create_hub, _), do: true @@ -699,11 +1052,32 @@ defimpl Canada.Can, for: Ret.OAuthProvider do def can?(_, _, _), do: false end -# Permissions for un-authenticated clients +# Permissions for app tokens and un-authenticated clients defimpl Canada.Can, for: Atom do - alias Ret.{AppConfig, Hub} + @allowed_app_token_actions [ + :get_rooms_created_by, + :get_favorite_rooms_of, + :get_public_rooms, + :create_hub, + :update_hub + ] + def can?(:reticulum_app_token, action, _) when action in @allowed_app_token_actions do + true + end - @object_actions [:spawn_and_move_media, :spawn_camera, :spawn_drawing, :pin_objects, :spawn_emoji, :fly] + # Bound hubs - Always prevent embedding and role assignment (since it's dictated by binding) + def can?(:reticulum_app_token, action, %Ret.Hub{hub_bindings: hub_bindings}) + when length(hub_bindings) > 0 and action in [:embed_hub, :update_roles], + do: false + + # Allow app tokens to act like owners/creators if the room has no bindings + def can?(:reticulum_app_token, action, %Ret.Hub{}) + when action in [:embed_hub, :update_roles], + do: true + + def can?(:reticulum_app_token, _, _), do: false + + alias Ret.{AppConfig, Hub} # Always deny access to non-enterable hubs def can?(_, :join_hub, %Ret.Hub{entry_mode: :deny}), do: false @@ -712,6 +1086,7 @@ defimpl Canada.Can, for: Atom do def can?(_, :join_hub, %Ret.Hub{hub_bindings: []}), do: !AppConfig.get_cached_config_value("features|require_account_for_join") + @object_actions [:spawn_and_move_media, :spawn_camera, :spawn_drawing, :pin_objects, :spawn_emoji, :fly] # Object permissions for anonymous users are based on member permission settings def can?(_account, action, hub) when action in @object_actions do hub |> Hub.has_member_permission?(action) diff --git a/lib/ret/json_schema_api_error_formatter.ex b/lib/ret/json_schema_api_error_formatter.ex index 7cb3a2f12..5ca6a6cd2 100644 --- a/lib/ret/json_schema_api_error_formatter.ex +++ b/lib/ret/json_schema_api_error_formatter.ex @@ -1,4 +1,5 @@ defmodule Ret.JsonSchemaApiErrorFormatter do + @moduledoc false def format(errors) do ExJsonSchema.Validator.Error.StringFormatter.format(errors) |> Enum.map(&{:MALFORMED_RECORD, elem(&1, 0), elem(&1, 1)}) diff --git a/lib/ret/media_resolver.ex b/lib/ret/media_resolver.ex index 787510d29..12c419215 100644 --- a/lib/ret/media_resolver.ex +++ b/lib/ret/media_resolver.ex @@ -34,7 +34,26 @@ defmodule Ret.MediaResolver do def resolve(%MediaResolverQuery{url: url} = query) when is_binary(url) do uri = url |> URI.parse() root_host = get_root_host(uri.host) - resolve(query |> Map.put(:url, uri), root_host) + query = Map.put(query, :url, uri) + + # TODO: We could end up running fallback_to_screenshot_opengraph_or_nothing + # twice in a row. These resolve functions can be simplified so that we can + # more easily track individual failures and only fallback when necessary. + # Also make sure they have a uniform response shape for indicating an + # error. + case resolve(query, root_host) do + :error -> + fallback_to_screenshot_opengraph_or_nothing(query) + + {:error, _reason} -> + fallback_to_screenshot_opengraph_or_nothing(query) + + {:commit, nil} -> + fallback_to_screenshot_opengraph_or_nothing(query) + + commit -> + commit + end end def resolve(%MediaResolverQuery{url: %URI{host: nil}}, _root_host) do @@ -191,6 +210,7 @@ defmodule Ret.MediaResolver do {:offline_stream, body} String.contains?(body, "HTTPError 429") -> + Statix.increment("ret.media_resolver.ytdl.rate_limited") {:rate_limited, body} true -> @@ -331,7 +351,12 @@ defmodule Ret.MediaResolver do end) end - defp resolve_non_video(%MediaResolverQuery{url: %URI{host: host} = uri, version: version}, _root_host) do + defp resolve_non_video(%MediaResolverQuery{} = query, _root_host) do + fallback_to_screenshot_opengraph_or_nothing(query) + end + + # TODO: Refactor this function + defp fallback_to_screenshot_opengraph_or_nothing(%MediaResolverQuery{url: %URI{host: host} = uri, version: version}) do photomnemonic_endpoint = module_config(:photomnemonic_endpoint) # Crawl og tags for hubs rooms + scenes @@ -339,7 +364,7 @@ defmodule Ret.MediaResolver do case uri |> URI.to_string() |> retry_head_then_get_until_success([{"Range", "bytes=0-32768"}]) do :error -> - nil + :error %HTTPoison.Response{headers: headers} -> content_type = headers |> content_type_from_headers @@ -375,7 +400,7 @@ defmodule Ret.MediaResolver do case Download.from(url, path: path) do {:ok, _path} -> {:ok, %{content_type: "image/png"}} - error -> {:error, error} + _error -> :error end end ) diff --git a/lib/ret/random_room_names.ex b/lib/ret/random_room_names.ex new file mode 100644 index 000000000..35531107a --- /dev/null +++ b/lib/ret/random_room_names.ex @@ -0,0 +1,673 @@ +defmodule Ret.RandomRoomNames do + @moduledoc false + + @adjectives [ + "able", + "absolute", + "acceptable", + "acclaimed", + "accomplished", + "accurate", + "aching", + "acrobatic", + "adorable", + "adventurous", + "basic", + "belated", + "beloved", + "calm", + "candid", + "capital", + "carefree", + "caring", + "cautious", + "celebrated", + "charming", + "daring", + "darling", + "dearest", + "each", + "eager", + "early", + "earnest", + "easy", + "easygoing", + "ecstatic", + "edible", + "fabulous", + "fair", + "faithful", + "familiar", + "famous", + "fancy", + "fantastic", + "far", + "generous", + "gentle", + "genuine", + "giant", + "handmade", + "handsome", + "handy", + "happy", + "icy", + "ideal", + "identical", + "keen", + "lasting", + "lavish", + "magnificent", + "majestic", + "mammoth", + "marvelous", + "natural", + "obedient", + "palatable", + "parched", + "passionate", + "pastel", + "peaceful", + "perfect", + "perfumed", + "quaint", + "qualified", + "radiant", + "rapid", + "rare", + "safe", + "sandy", + "satisfied", + "scaly", + "scarce", + "scared", + "scary", + "scented", + "scientific", + "secret", + "sentimental", + "talkative", + "tangible", + "tart", + "tasty", + "tattered", + "teeming", + "ultimate", + "uncommon", + "unconscious", + "understated", + "warm", + "active", + "adept", + "admirable", + "admired", + "adorable", + "adored", + "advanced", + "affectionate", + "beneficial", + "best", + "better", + "big", + "cheerful", + "cheery", + "chief", + "chilly", + "classic", + "clean", + "clear", + "clever", + "decent", + "decisive", + "deep", + "defiant", + "definitive", + "delectable", + "delicious", + "elaborate", + "elastic", + "elated", + "elegant", + "elementary", + "elliptical", + "fast", + "favorable", + "favorite", + "fearless", + "gifted", + "glamorous", + "gleaming", + "glittering", + "harmonious", + "imaginative", + "immense", + "jealous", + "kind", + "leafy", + "legal", + "mature", + "mean", + "nautical", + "neat", + "necessary", + "needy", + "oddball", + "offbeat", + "periodic", + "perky", + "personal", + "pertinent", + "petty", + "quarterly", + "ready", + "real", + "realistic", + "reasonable", + "regal", + "serene", + "shabby", + "sharp", + "shiny", + "showy", + "shy", + "silky", + "tempting", + "tense", + "terrific", + "testy", + "thankful", + "uniform", + "unique", + "vast", + "weary", + "wee", + "welcome", + "agile", + "alarmed", + "alert", + "alive", + "bleak", + "blissful", + "blushing", + "coarse", + "colorful", + "colossal", + "comfortable", + "compassionate", + "complete", + "delightful", + "dense", + "dependable", + "dependent", + "descriptive", + "detailed", + "determined", + "devoted", + "different", + "eminent", + "emotional", + "enchanted", + "enchanting", + "energetic", + "enormous", + "fine", + "finished", + "firm", + "firsthand", + "fixed", + "flashy", + "flawless", + "glorious", + "glossy", + "golden", + "good", + "gorgeous", + "graceful", + "healthy", + "heartfelt", + "hearty", + "helpful", + "impartial", + "impressive", + "jolly", + "jovial", + "lighthearted", + "likable", + "lined", + "mellow", + "melodic", + "memorable", + "mild", + "new", + "opulent", + "playful", + "pleasant", + "pleasing", + "plump", + "plush", + "polished", + "polite", + "reliable", + "relieved", + "remarkable", + "remote", + "respectful", + "responsible", + "simple", + "simplistic", + "sizzling", + "sleepy", + "slight", + "slim", + "smart", + "smooth", + "snappy", + "snoopy", + "thirsty", + "this", + "thorough", + "those", + "thoughtful", + "united", + "vibrant", + "vicious", + "wellmade", + "whimsical", + "whirlwind", + "zesty", + "amazing", + "ambitious", + "ample", + "amused", + "amusing", + "ancient", + "angelic", + "antique", + "bold", + "bossy", + "both", + "bouncy", + "bountiful", + "complex", + "conscious", + "considerate", + "constant", + "content", + "conventional", + "cooked", + "cool", + "cooperative", + "diligent", + "dimwitted", + "direct", + "discrete", + "envious", + "essential", + "ethical", + "euphoric", + "flippant", + "fluffy", + "flustered", + "focused", + "fond", + "gracious", + "grand", + "grandiose", + "granular", + "grateful", + "grave", + "great", + "hidden", + "high", + "hilarious", + "homely", + "incomparable", + "incredible", + "infamous", + "joyful", + "lively", + "loathsome", + "lonely", + "long", + "mindless", + "miniature", + "minor", + "misty", + "next", + "nice", + "nifty", + "nimble", + "orderly", + "organic", + "ornate", + "popular", + "posh", + "positive", + "potable", + "powerful", + "powerless", + "precious", + "present", + "prestigious", + "quick", + "rewarding", + "rich", + "right", + "sociable", + "soft", + "solid", + "some", + "sophisticated", + "soulful", + "sparkling", + "spectacular", + "speedy", + "spicy", + "spiffy", + "spirited", + "spiteful", + "splendid", + "spotless", + "spry", + "thrifty", + "tidy", + "tight", + "timely", + "tinted", + "unruly", + "untimely", + "violet", + "wicked", + "wide", + "wild", + "willing", + "winding", + "windy", + "zigzag", + "apprehensive", + "appropriate", + "artistic", + "assured", + "astonishing", + "bright", + "brilliant", + "bronze", + "coordinated", + "courageous", + "courteous", + "crafty", + "crazy", + "creamy", + "creative", + "crisp", + "distant", + "distinct", + "downright", + "evergreen", + "everlasting", + "every", + "evil", + "excellent", + "excitable", + "exemplary", + "exhausted", + "forthright", + "fortunate", + "fragrant", + "frank", + "free", + "frequent", + "fresh", + "friendly", + "frightened", + "frigid", + "gripping", + "grounded", + "honest", + "honorable", + "honored", + "hopeful", + "hospitable", + "hot", + "huge", + "infatuated", + "infinite", + "informal", + "insistent", + "instructive", + "juicy", + "jumbo", + "knowing", + "knowledgeable", + "longterm", + "loud", + "lovable", + "loving", + "modern", + "modest", + "monumental", + "normal", + "notable", + "outgoing", + "precious", + "pretty", + "prickly", + "primary", + "pristine", + "private", + "prize", + "productive", + "profitable", + "quiet", + "quintessential", + "roasted", + "robust", + "square", + "squiggly", + "stable", + "staid", + "starry", + "steel", + "stimulating", + "striking", + "striped", + "strong", + "studious", + "stunning", + "tough", + "trained", + "treasured", + "tremendous", + "triangular", + "tricky", + "unused", + "unusual", + "upbeat", + "virtual", + "witty", + "wonderful", + "wooden", + "worldly", + "youthful", + "attached", + "attentive", + "attractive", + "austere", + "authentic", + "automatic", + "aware", + "awesome", + "bubbly", + "bustling", + "busy", + "buttery", + "cuddly", + "cultured", + "curly", + "curvy", + "cute", + "cylindrical", + "downright", + "dramatic", + "excited", + "exciting", + "exotic", + "experienced", + "expert", + "frosty", + "fruitful", + "full", + "fumbling", + "funny", + "fussy", + "growing", + "grown", + "gummy", + "humble", + "humongous", + "hungry", + "intelligent", + "interesting", + "known", + "kooky", + "loyal", + "lucky", + "luminous", + "lustrous", + "luxurious", + "multicolored", + "mysterious", + "noteworthy", + "numb", + "nutritious", + "outstanding", + "overjoyed", + "proper", + "proud", + "prudent", + "punctual", + "puny", + "pure", + "puzzled", + "puzzling", + "quirky", + "stupendous", + "sturdy", + "stylish", + "subdued", + "subtle", + "sunny", + "super", + "superb", + "supportive", + "surprised", + "sweet", + "swift", + "sympathetic", + "trivial", + "trusting", + "trustworthy", + "trusty", + "truthful", + "twin", + "usable", + "used", + "useful", + "utilized", + "vital", + "vivid", + "worried", + "worthwhile", + "worthy", + "writhing", + "wry", + "yummy", + "chocolate", + "crimson", + "cyan", + "fuchsia", + "gold", + "honeydew", + "lime", + "linen", + "magenta", + "olive", + "peru", + "salmon", + "seashell", + "sienna", + "snow", + "thistle", + "tomato", + "transparent", + "turquoise", + "violet" + ] + + @nouns [ + "space", + "land", + "world", + "universe", + "plane", + "room", + "nation", + "plaza", + "gathering", + "meetup", + "get together", + "conclave", + "party", + "domain", + "dominion", + "realm", + "square", + "commons", + "park", + "cosmos", + "sphere", + "terrain", + "spot", + "zone", + "area", + "tract", + "turf", + "place", + "territory", + "volume", + "camp", + "picnic", + "outing", + "vacation", + "adventure", + "exploration", + "outing", + "walkabout", + "safari", + "venture", + "roundtable", + "barbecue", + "celebration", + "festivity", + "gala", + "shindig", + "social", + "convention", + "assembly", + "congregation", + "rendezvous", + "huddle", + "meet", + "soiree" + ] + + defp random_from(words) do + Enum.at(words, :rand.uniform(length(words)) - 1) + end + + def generate_room_name() do + [@adjectives, @adjectives, @nouns] + |> Stream.map(&random_from/1) + |> Stream.map(&String.capitalize(&1)) + |> Enum.join(" ") + end +end diff --git a/lib/ret/scene.ex b/lib/ret/scene.ex index cbc4cbe5e..03b48da6e 100644 --- a/lib/ret/scene.ex +++ b/lib/ret/scene.ex @@ -9,6 +9,7 @@ end defmodule Ret.Scene do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Ret.{Repo, Scene, SceneListing, Project, Storage} alias Ret.Scene.{SceneSlug} @@ -59,6 +60,17 @@ defmodule Ret.Scene do SceneListing |> Repo.get_by(scene_listing_sid: sid) |> Repo.preload(scene: Scene.scene_preloads()) end + def projectless_scenes_for_account(account) do + Repo.all( + from(s in Scene, + left_join: project in assoc(s, :project), + where: s.account_id == ^account.account_id and is_nil(s.scene_owned_file_id) and is_nil(project), + preload: ^Scene.scene_preloads(), + order_by: [desc: s.updated_at] + ) + ) + end + def to_sid(nil), do: nil def to_sid(%Scene{} = scene), do: scene.scene_sid def to_sid(%SceneListing{} = scene_listing), do: scene_listing.scene_listing_sid @@ -195,9 +207,9 @@ defmodule Ret.Scene do |> maybe_add_scene_sid_to_changeset |> unique_constraint(:scene_sid) |> put_assoc(:account, account) - |> put_assoc(:model_owned_file, model_owned_file) - |> put_assoc(:screenshot_owned_file, screenshot_owned_file) - |> put_assoc(:scene_owned_file, scene_owned_file) + |> maybe_put_assoc(:model_owned_file, model_owned_file) + |> maybe_put_assoc(:screenshot_owned_file, screenshot_owned_file) + |> maybe_put_assoc(:scene_owned_file, scene_owned_file) |> SceneSlug.maybe_generate_slug() end @@ -249,4 +261,12 @@ defmodule Ret.Scene do scene_sid = changeset |> get_field(:scene_sid) || Ret.Sids.generate_sid() put_change(changeset, :scene_sid, scene_sid) end + + defp maybe_put_assoc(changeset, _key, nil) do + changeset + end + + defp maybe_put_assoc(changeset, key, value) do + changeset |> put_assoc(key, value) + end end diff --git a/lib/ret/storage.ex b/lib/ret/storage.ex index f90022205..3f79727eb 100644 --- a/lib/ret/storage.ex +++ b/lib/ret/storage.ex @@ -111,6 +111,18 @@ defmodule Ret.Storage do |> Enum.into(%{}) end + # Similar to promote above, but allows for passing nil. Useful for optional upload fields + def promote_optional(map, %Account{} = account) when is_map(map) do + map + |> Enum.map(fn + {k, {nil, nil}} -> {k, {:ok, nil}} + {k, {nil, nil, nil}} -> {k, {:ok, nil}} + {k, {id, key}} -> {k, promote(id, key, nil, account)} + {k, {id, key, promotion_token}} -> {k, promote(id, key, promotion_token, account)} + end) + |> Enum.into(%{}) + end + defp promote_or_return_owned_file(%OwnedFile{} = owned_file, _id, _key, _promotion_token, _account) do {:ok, owned_file} end diff --git a/lib/ret_web/auth_error_handler.ex b/lib/ret_web/auth_error_handler.ex index 717125b16..11d81664a 100644 --- a/lib/ret_web/auth_error_handler.ex +++ b/lib/ret_web/auth_error_handler.ex @@ -1,4 +1,5 @@ defmodule RetWeb.Guardian.AuthErrorHandler do + @moduledoc false import Plug.Conn def auth_error(conn, {type, _reason}, _opts) do diff --git a/lib/ret_web/auth_optional_pipeline.ex b/lib/ret_web/auth_optional_pipeline.ex index fe3fef456..bd861bfc7 100644 --- a/lib/ret_web/auth_optional_pipeline.ex +++ b/lib/ret_web/auth_optional_pipeline.ex @@ -1,4 +1,5 @@ defmodule RetWeb.Guardian.AuthOptionalPipeline do + @moduledoc false use Guardian.Plug.Pipeline, otp_app: :ret, module: Ret.Guardian, diff --git a/lib/ret_web/controllers/api/v1/hub_controller.ex b/lib/ret_web/controllers/api/v1/hub_controller.ex index f9ae36661..c873a85b3 100644 --- a/lib/ret_web/controllers/api/v1/hub_controller.ex +++ b/lib/ret_web/controllers/api/v1/hub_controller.ex @@ -68,8 +68,8 @@ defmodule RetWeb.Api.V1.HubController do hub |> Hub.add_attrs_to_changeset(hub_params) |> maybe_add_new_scene(scene) - |> maybe_add_member_permissions(hub, hub_params) - |> maybe_add_promotion(account, hub, hub_params) + |> Hub.maybe_add_member_permissions(hub, hub_params) + |> Hub.maybe_add_promotion(account, hub, hub_params) hub = changeset |> Repo.update!() |> Repo.preload(Hub.hub_preloads()) @@ -80,16 +80,6 @@ defmodule RetWeb.Api.V1.HubController do defp maybe_add_new_scene(changeset, scene), do: changeset |> Hub.add_new_scene_to_changeset(scene) - defp maybe_add_member_permissions(changeset, hub, %{"member_permissions" => %{}} = hub_params), - do: changeset |> Hub.add_member_permissions_update_to_changeset(hub, hub_params) - - defp maybe_add_member_permissions(changeset, _hub, _), do: changeset - - defp maybe_add_promotion(changeset, account, hub, %{"allow_promotion" => _} = hub_params), - do: changeset |> Hub.maybe_add_promotion_to_changeset(account, hub, hub_params) - - defp maybe_add_promotion(changeset, _account, _hub, _), do: changeset - def delete(conn, %{"id" => hub_sid}) do Hub |> Repo.get_by(hub_sid: hub_sid) diff --git a/lib/ret_web/controllers/api/v1/media_controller.ex b/lib/ret_web/controllers/api/v1/media_controller.ex index a1675cda9..0c340f894 100644 --- a/lib/ret_web/controllers/api/v1/media_controller.ex +++ b/lib/ret_web/controllers/api/v1/media_controller.ex @@ -1,6 +1,7 @@ defmodule RetWeb.Api.V1.MediaController do use RetWeb, :controller use Retry + alias Ret.Statix def create(conn, %{"media" => %{"url" => url, "quality" => quality}, "version" => version}), do: resolve_and_render(conn, url, version, String.to_atom(quality)) @@ -73,6 +74,14 @@ defmodule RetWeb.Api.V1.MediaController do end defp resolve_and_render(conn, url, version, quality \\ nil) do + query = query_for(conn, url, version, quality) + value = Cachex.fetch(:media_urls, query) + maybe_do_telemetry(value) + maybe_bump_ttl(value, query) + render_resolved_media_or_error(conn, value) + end + + defp query_for(conn, url, version, quality) do quality = quality || default_quality(conn) ua = @@ -83,39 +92,12 @@ defmodule RetWeb.Api.V1.MediaController do supports_webm = ua.family != "Safari" && ua.family != "Mobile Safari" - query = %Ret.MediaResolverQuery{ + %Ret.MediaResolverQuery{ url: url, supports_webm: supports_webm, quality: quality, version: version } - - case Cachex.fetch(:media_urls, query) do - {_status, nil} -> - conn |> send_resp(404, "") - - {_status, %Ret.ResolvedMedia{ttl: ttl} = resolved_media} -> - if ttl do - Cachex.expire(:media_urls, query, :timer.seconds(ttl / 1000)) - end - - render_resolved_media(conn, resolved_media) - - {:error, e} -> - conn |> send_resp(500, e) - - _ -> - conn |> send_resp(500, "Error resolving media") - end - end - - defp render_resolved_media(conn, %Ret.ResolvedMedia{uri: uri, audio_uri: audio_uri, meta: meta}) - when audio_uri != nil do - conn |> render("show.json", origin: uri |> URI.to_string(), origin_audio: audio_uri |> URI.to_string(), meta: meta) - end - - defp render_resolved_media(conn, %Ret.ResolvedMedia{uri: uri, meta: meta}) do - conn |> render("show.json", origin: uri |> URI.to_string(), meta: meta) end defp default_quality(conn) do @@ -131,4 +113,59 @@ defmodule RetWeb.Api.V1.MediaController do :high end end + + defp maybe_do_telemetry({:commit, nil}), do: Statix.increment("ret.media_resolver.404") + defp maybe_do_telemetry({:commit, %Ret.ResolvedMedia{}}), do: Statix.increment("ret.media_resolver.ok") + defp maybe_do_telemetry({:error, _reason}), do: Statix.increment("ret.media_resolver.unknown_error") + defp maybe_do_telemetry({:commit, :error}), do: Statix.increment("ret.media_resolver.500") + defp maybe_do_telemetry({:commit, {:error, _reason}}), do: Statix.increment("ret.media_resolver.500") + defp maybe_do_telemetry(_), do: nil + + defp maybe_bump_ttl({_status, %Ret.ResolvedMedia{ttl: ttl}}, query) do + if ttl do + Cachex.expire(:media_urls, query, :timer.seconds(ttl / 1000)) + end + end + + defp maybe_bump_ttl(_value, _query), do: nil + + defp render_resolved_media_or_error(conn, {_status, nil}) do + send_resp(conn, 404, "") + end + + defp render_resolved_media_or_error(conn, {_status, %Ret.ResolvedMedia{} = resolved_media}) do + render_resolved_media(conn, resolved_media) + end + + # This is an error response that we have cached ourselves + defp render_resolved_media_or_error(conn, {_status, :error}) do + send_resp(conn, 500, "An error occured during media resolution") + end + + # This is an error response that we have cached ourselves + defp render_resolved_media_or_error(conn, {_status, {:error, _reason}}) do + send_resp(conn, 500, "An error occured during media resolution") + end + + # This is an unexpected error response from Cachex + defp render_resolved_media_or_error(conn, {:error, _reason}) do + Statix.increment("ret.media_resolver.unknown_cachex_error") + send_resp(conn, 500, "An unexpected error occurred during media resolution.") + end + + # This is an unexpected response from Cachex + defp render_resolved_media_or_error(conn, _) do + # We do not expect this code to run, so if it happens, something went wrong + Statix.increment("ret.media_resolver.unknown_error") + send_resp(conn, 500, "An unexpected error occurred during media resolution.") + end + + defp render_resolved_media(conn, %Ret.ResolvedMedia{uri: uri, audio_uri: audio_uri, meta: meta}) + when audio_uri != nil do + conn |> render("show.json", origin: uri |> URI.to_string(), origin_audio: audio_uri |> URI.to_string(), meta: meta) + end + + defp render_resolved_media(conn, %Ret.ResolvedMedia{uri: uri, meta: meta}) do + conn |> render("show.json", origin: uri |> URI.to_string(), meta: meta) + end end diff --git a/lib/ret_web/controllers/api/v1/scene_controller.ex b/lib/ret_web/controllers/api/v1/scene_controller.ex index b294b64b7..cbe3748a1 100644 --- a/lib/ret_web/controllers/api/v1/scene_controller.ex +++ b/lib/ret_web/controllers/api/v1/scene_controller.ex @@ -30,6 +30,11 @@ defmodule RetWeb.Api.V1.SceneController do end end + def index_projectless(conn, _params) do + account = Guardian.Plug.current_resource(conn) + conn |> render("index.json", scenes: Scene.projectless_scenes_for_account(account), account: account) + end + def update(conn, %{"id" => scene_sid, "scene" => params}) do case scene_sid |> get_scene() do %Scene{} = scene -> create_or_update(conn, params, scene) @@ -88,7 +93,7 @@ defmodule RetWeb.Api.V1.SceneController do defp create_or_update(conn, params, scene, account) do owned_file_results = - Storage.promote( + Storage.promote_optional( %{ model: {params["model_file_id"], params["model_file_token"]}, screenshot: {params["screenshot_file_id"], params["screenshot_file_token"]}, diff --git a/lib/ret_web/controllers/api/v2/credentials_controller.ex b/lib/ret_web/controllers/api/v2/credentials_controller.ex new file mode 100644 index 000000000..757a5b572 --- /dev/null +++ b/lib/ret_web/controllers/api/v2/credentials_controller.ex @@ -0,0 +1,129 @@ +defmodule RetWeb.Api.V2.CredentialsController do + use RetWeb, :controller + + alias Ret.{Repo} + alias Ret.Api.Credentials + alias Ecto.Changeset + + import Ret.Api.TokenUtils, + only: [to_claims: 2, authed_create_credentials: 2, authed_list_credentials: 2, authed_revoke_credentials: 2] + + # Limit to 1 TPS + plug(RetWeb.Plugs.RateLimit when action in [:create, :update]) + + def index(conn, %{"app" => _anything} = _params) do + handle_list_credentials_result(conn, authed_list_credentials(Guardian.Plug.current_resource(conn), :app)) + end + + def index(conn, _params) do + handle_list_credentials_result(conn, authed_list_credentials(Guardian.Plug.current_resource(conn), :account)) + end + + def show(conn, %{"id" => credentials_sid}) do + case Repo.get_by(Credentials, api_credentials_sid: credentials_sid) do + nil -> + render_errors(conn, 400, {:error, "Invalid request"}) + + credentials -> + conn + |> put_resp_header("content-type", "application/json") + |> put_status(200) + |> render("show.json", credentials: credentials) + end + end + + def create(conn, params) do + account = Guardian.Plug.current_resource(conn) + + case to_claims(account, params) do + {:ok, claims} -> + handle_create_credentials_result(conn, authed_create_credentials(account, claims)) + + {:error, error_list} -> + render_errors(conn, 400, error_list) + end + end + + def update(conn, %{"id" => credentials_sid, "revoke" => _anything}) do + account = Guardian.Plug.current_resource(conn) + + case Credentials.query() + |> Credentials.where_sid_is(credentials_sid) + |> Repo.one() do + nil -> + render_errors(conn, 400, {:error, "Invalid request"}) + + credentials -> + handle_revoke_credentials_result(conn, authed_revoke_credentials(account, credentials)) + end + end + + defp handle_list_credentials_result(conn, {:error, :unauthorized}) do + render_errors(conn, 401, {:unauthorized, "You do not have permission to view these credentials."}) + end + + defp handle_list_credentials_result(conn, {:error, reason}) do + render_errors(conn, 400, reason) + end + + defp handle_list_credentials_result(conn, credentials) do + conn + |> put_resp_header("content-type", "application/json") + |> put_status(200) + |> render("index.json", credentials: credentials) + end + + defp handle_create_credentials_result(conn, {:error, :unauthorized}) do + render_errors(conn, 401, {:unauthorized, "You do not have permission to create these credentials."}) + end + + defp handle_create_credentials_result(conn, {:error, reason}) do + render_errors(conn, 400, reason) + end + + defp handle_create_credentials_result(conn, {:ok, token, _claims}) do + # Lookup credentials because token creation returns the + # claims map, not the credentials object written to DB. + credentials = + Credentials.query() + |> Credentials.where_token_hash_is(Ret.Crypto.hash(token)) + |> Repo.one() + + conn + |> put_resp_header("content-type", "application/json") + |> put_status(200) + |> render("show.json", token: token, credentials: credentials) + end + + defp handle_revoke_credentials_result(conn, {:error, :unauthorized}) do + render_errors(conn, 401, {:unauthorized, "You do not have permission to revoke these credentials."}) + end + + defp handle_revoke_credentials_result(conn, {:error, reason}) do + render_errors(conn, 400, reason) + end + + defp handle_revoke_credentials_result(conn, {:ok, credentials}) do + conn + |> put_resp_header("content-type", "application/json") + |> put_status(200) + |> render("show.json", credentials: credentials) + end + + defp render_errors(conn, status, errors) when is_list(errors) do + conn + |> put_resp_header("content-type", "application/json") + |> put_status(status) + |> render("errors.json", errors: errors) + end + + defp render_errors(conn, status, %Changeset{} = changeset) do + render_errors(conn, status, + errors: changeset |> Ecto.Changeset.traverse_errors(fn {err, _opts} -> err end) |> Enum.to_list() + ) + end + + defp render_errors(conn, status, error) do + render_errors(conn, status, List.wrap(error)) + end +end diff --git a/lib/ret_web/endpoint.ex b/lib/ret_web/endpoint.ex index ec120ca14..dff9480b0 100644 --- a/lib/ret_web/endpoint.ex +++ b/lib/ret_web/endpoint.ex @@ -1,6 +1,7 @@ defmodule RetWeb.Endpoint do use Phoenix.Endpoint, otp_app: :ret use Sentry.Phoenix.Endpoint + use Absinthe.Phoenix.Endpoint socket("/socket", RetWeb.SessionSocket, websocket: [check_origin: {RetWeb.Endpoint, :allowed_origin?, []}]) diff --git a/lib/ret_web/middleware.ex b/lib/ret_web/middleware.ex new file mode 100644 index 000000000..7d7416bc9 --- /dev/null +++ b/lib/ret_web/middleware.ex @@ -0,0 +1,30 @@ +defmodule RetWeb.Middleware do + @moduledoc "Adds absinthe middleware on matching fields/objects" + + alias RetWeb.Middleware.{ + HandleApiTokenAuthErrors, + HandleChangesetErrors, + StartTiming, + EndTiming, + InspectTiming + } + + @timing_ids [ + :my_rooms, + :public_rooms, + :favorite_rooms, + :create_room, + :update_room + ] + + def build_middleware(middleware, %{identifier: field_id} = _field, _object) do + include_timing = field_id in @timing_ids + + if(include_timing, do: [StartTiming], else: []) ++ + [HandleApiTokenAuthErrors] ++ + middleware ++ + [HandleChangesetErrors] ++ + if(include_timing, do: [EndTiming], else: []) ++ + if(include_timing, do: [InspectTiming], else: []) + end +end diff --git a/lib/ret_web/middleware/handle_api_token_auth_errors.ex b/lib/ret_web/middleware/handle_api_token_auth_errors.ex new file mode 100644 index 000000000..510464fe3 --- /dev/null +++ b/lib/ret_web/middleware/handle_api_token_auth_errors.ex @@ -0,0 +1,41 @@ +defmodule RetWeb.Middleware.HandleApiTokenAuthErrors do + @moduledoc false + + @behaviour Absinthe.Middleware + + import RetWeb.Middleware.PutErrorResult, only: [put_error_result: 3] + + alias Ret.Api.Credentials + + def call(%{state: :resolved} = resolution, _) do + resolution + end + + # Don't enforce authentication on introspection queries + # See Absinthe.Introspection.type? + # https://github.com/absinthe-graphql/absinthe/blob/cdb8c39beb6a79b03a5095fffbe761e0dd9918ac/lib/absinthe/introspection.ex#L106 + def call(%{parent_type: %{name: "__" <> _}} = resolution, _) do + resolution + end + + def call(%{context: %{api_token_auth_errors: errors}} = resolution, _) when is_list(errors) and length(errors) > 0 do + {type, reason} = Enum.at(errors, 0) + put_error_result(resolution, type, reason) + end + + def call(%{context: %{credentials: nil}} = resolution, _) do + put_error_result( + resolution, + :api_access_token_not_found, + "Failed to find api access token when searching for header 'Authorization: Bearer '" + ) + end + + def call(%{context: %{credentials: %Credentials{is_revoked: true}}} = resolution, _) do + put_error_result(resolution, :token_revoked, "Token is revoked") + end + + def call(%{context: %{credentials: %Credentials{}}} = resolution, _) do + resolution + end +end diff --git a/lib/ret_web/middleware/handle_changeset_errors.ex b/lib/ret_web/middleware/handle_changeset_errors.ex new file mode 100644 index 000000000..26839584d --- /dev/null +++ b/lib/ret_web/middleware/handle_changeset_errors.ex @@ -0,0 +1,15 @@ +defmodule RetWeb.Middleware.HandleChangesetErrors do + @moduledoc false + @behaviour Absinthe.Middleware + def call(resolution, _) do + %{resolution | errors: Enum.flat_map(resolution.errors, &handle_error/1)} + end + + defp handle_error(%Ecto.Changeset{} = changeset) do + changeset + |> Ecto.Changeset.traverse_errors(fn {err, _opts} -> err end) + |> Enum.map(fn {k, v} -> "#{k}: #{v}" end) + end + + defp handle_error(error), do: [error] +end diff --git a/lib/ret_web/middleware/put_error_result.ex b/lib/ret_web/middleware/put_error_result.ex new file mode 100644 index 000000000..fff8678de --- /dev/null +++ b/lib/ret_web/middleware/put_error_result.ex @@ -0,0 +1,12 @@ +defmodule RetWeb.Middleware.PutErrorResult do + @moduledoc "Helper for returning auth errors in a uniform way in graphql api" + + import Absinthe.Resolution, only: [put_result: 2] + + def put_error_result(resolution, type, message) do + put_result( + resolution, + {:error, [type: type, message: message]} + ) + end +end diff --git a/lib/ret_web/middleware/timing.ex b/lib/ret_web/middleware/timing.ex new file mode 100644 index 000000000..a50f4498f --- /dev/null +++ b/lib/ret_web/middleware/timing.ex @@ -0,0 +1,68 @@ +defmodule RetWeb.Middleware.TimingUtil do + @moduledoc false + def add_timing_info(%Absinthe.Resolution{private: private} = resolution, identifier, key, value) do + timing = Map.get(private, :timing) || %{} + info = Map.put(Map.get(timing, identifier) || %{}, key, value) + + %{ + resolution + | private: Map.put(private, :timing, Map.put(timing, identifier, info)) + } + end +end + +defmodule RetWeb.Middleware.StartTiming do + @moduledoc false + + import RetWeb.Middleware.TimingUtil, only: [add_timing_info: 4] + + @behaviour Absinthe.Middleware + def call(resolution, _) do + add_timing_info(resolution, resolution.definition.schema_node.identifier, :started_at, NaiveDateTime.utc_now()) + end +end + +defmodule RetWeb.Middleware.EndTiming do + @moduledoc false + + import RetWeb.Middleware.TimingUtil, only: [add_timing_info: 4] + + @behaviour Absinthe.Middleware + def call(resolution, _) do + add_timing_info(resolution, resolution.definition.schema_node.identifier, :ended_at, NaiveDateTime.utc_now()) + end +end + +defmodule RetWeb.Middleware.InspectTiming do + @moduledoc false + + @behaviour Absinthe.Middleware + def call(resolution, _) do + case resolution do + %{private: %{timing: timing}} -> + log_timing_info(timing) + nil + + _ -> + nil + end + + resolution + end + + defp log_timing_info(_timing) do + nil + end + + # # TODO: Log these metrics with something like :telemetry or Statix + # defp log_timing_info(timing) do + # Enum.each(timing, fn + # {identifier, %{started_at: started_at, ended_at: ended_at}} -> + # diff = NaiveDateTime.diff(ended_at, started_at, :microsecond) + # IO.puts("#{Atom.to_string(identifier)} took #{diff} microseconds to run.") + + # _ -> + # nil + # end) + # end +end diff --git a/lib/ret_web/plugs/add_absinthe_context.ex b/lib/ret_web/plugs/add_absinthe_context.ex new file mode 100644 index 000000000..f84a2621a --- /dev/null +++ b/lib/ret_web/plugs/add_absinthe_context.ex @@ -0,0 +1,25 @@ +defmodule RetWeb.AddAbsintheContext do + @moduledoc false + @behaviour Plug + + def init(opts), do: opts + + def call(conn, _) do + Absinthe.Plug.put_options(conn, context: build_context(conn)) + end + + defp build_context(conn) do + auth_errors = conn.assigns[:api_token_auth_errors] || [] + + case Guardian.Plug.current_claims(conn) do + {:error, :invalid_token} -> + %{api_token_auth_errors: [{:invalid_token, "Invalid token error. Could not find credentials."}] ++ auth_errors} + + credentials -> + %{ + api_token_auth_errors: auth_errors, + credentials: credentials + } + end + end +end diff --git a/lib/ret_web/plugs/api_token_auth_pipeline.ex b/lib/ret_web/plugs/api_token_auth_pipeline.ex new file mode 100644 index 000000000..8747f7a18 --- /dev/null +++ b/lib/ret_web/plugs/api_token_auth_pipeline.ex @@ -0,0 +1,29 @@ +defmodule RetWeb.ApiTokenAuthPipeline do + @moduledoc false + use Guardian.Plug.Pipeline, + otp_app: :ret, + module: Ret.Api.Token, + error_handler: RetWeb.ApiTokenAuthErrorHandler + + plug(Guardian.Plug.VerifyHeader, halt: false) +end + +defmodule RetWeb.ApiTokenAuthErrorHandler do + @moduledoc false + + def auth_error(conn, {failure_type, %ArgumentError{message: reason}}, _opts) do + append_error(conn, failure_type, reason) + end + + def auth_error(conn, {failure_type, reason}, _opts) do + append_error(conn, failure_type, reason) + end + + def append_error(conn, failure_type, reason) do + Plug.Conn.assign( + conn, + :api_token_auth_errors, + (conn.assigns[:api_token_auth_errors] || []) ++ [{failure_type, reason}] + ) + end +end diff --git a/lib/ret_web/plugs/require_public_api_access.ex b/lib/ret_web/plugs/require_public_api_access.ex new file mode 100644 index 000000000..7ec07cec1 --- /dev/null +++ b/lib/ret_web/plugs/require_public_api_access.ex @@ -0,0 +1,13 @@ +defmodule RetWeb.Plugs.RequirePublicApiAccess do + import Plug.Conn + + def init([]), do: [] + + def call(conn, []) do + if Ret.AppConfig.get_config_bool("features|public_api_access") do + conn + else + conn |> send_resp(404, "") |> halt() + end + end +end diff --git a/lib/ret_web/resolvers/resolver_error.ex b/lib/ret_web/resolvers/resolver_error.ex new file mode 100644 index 000000000..422f93365 --- /dev/null +++ b/lib/ret_web/resolvers/resolver_error.ex @@ -0,0 +1,6 @@ +defmodule RetWeb.Resolvers.ResolverError do + @moduledoc false + def resolver_error(type, reason) do + {:error, [type: type, message: reason]} + end +end diff --git a/lib/ret_web/resolvers/room_resolver.ex b/lib/ret_web/resolvers/room_resolver.ex new file mode 100644 index 000000000..7a57ecb4c --- /dev/null +++ b/lib/ret_web/resolvers/room_resolver.ex @@ -0,0 +1,143 @@ +defmodule RetWeb.Resolvers.RoomResolver do + @moduledoc """ + Resolvers for room queries and mutations via the graphql API + """ + alias Ret.Hub + alias Ret.Api.Credentials + import RetWeb.Resolvers.ResolverError, only: [resolver_error: 2] + + def my_rooms(_parent, _args, %{ + context: %{ + credentials: %Credentials{ + subject_type: :app + } + } + }) do + resolver_error(:not_implemented, "Not implemented for app tokens") + end + + def my_rooms(_parent, args, %{ + context: %{ + credentials: + %Credentials{ + subject_type: :account, + account: account + } = credentials + } + }) do + Ret.Api.Rooms.authed_get_rooms_created_by(account, credentials, args) + end + + def my_rooms(_parent, _args, _resolutions) do + resolver_error(:unauthorized, "Unauthorized access") + end + + def favorite_rooms(_parent, _args, %{ + context: %{ + credentials: %Credentials{ + subject_type: :app + } + } + }) do + resolver_error(:not_implemented, "Not implemented for app tokens") + end + + def favorite_rooms(_parent, args, %{ + context: %{ + credentials: + %Credentials{ + subject_type: :account, + account: account + } = credentials + } + }) do + Ret.Api.Rooms.authed_get_favorite_rooms_of(account, credentials, args) + end + + def favorite_rooms(_parent, _args, _resolutions) do + resolver_error(:unauthorized, "Unauthorized access") + end + + def public_rooms(_parent, args, %{ + context: %{ + credentials: %Credentials{} = credentials + } + }) do + Ret.Api.Rooms.authed_get_public_rooms(credentials, args) + end + + def public_rooms(_, _, _) do + resolver_error(:unauthorized, "Unauthorized access") + end + + def create_room(_parent, args, %{ + context: %{ + credentials: %Credentials{} = credentials + } + }) do + Ret.Api.Rooms.authed_create_room(credentials, args) + end + + def create_room(_parent, _args, _resolutions) do + resolver_error(:unauthorized, "Unauthorized access") + end + + def embed_token(hub, _args, %{ + context: %{ + credentials: %Credentials{} = credentials + } + }) do + Ret.Api.Rooms.authed_get_embed_token(credentials, hub) + end + + def embed_token(_hub, _args, _resolutions) do + resolver_error(:unauthorized, "Unauthorized access") + end + + def port(_hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.janus_port()} + end + + def turn(_hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.generate_turn_info()} + end + + def member_permissions(hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.member_permissions_for_hub_as_atoms(hub)} + end + + def room_size(hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.room_size_for(hub)} + end + + def member_count(hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.member_count_for(hub)} + end + + def lobby_count(hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.lobby_count_for(hub)} + end + + def scene(hub, _args, _resolutions) do + # No permission check needed + {:ok, Hub.scene_or_scene_listing_for(hub)} + end + + def update_room(_parent, %{id: hub_sid} = args, %{ + context: %{ + credentials: %Credentials{} = credentials + } + }) do + Ret.Api.Rooms.authed_update_room(hub_sid, credentials, args) + end + + def update_room(_parent, _args, _resolutions) do + resolver_error(:unauthorized, "Unauthorized access") + end +end diff --git a/lib/ret_web/router.ex b/lib/ret_web/router.ex index 56e94c89d..7a3ac49ef 100644 --- a/lib/ret_web/router.ex +++ b/lib/ret_web/router.ex @@ -32,6 +32,10 @@ defmodule RetWeb.Router do plug(:accepts, ["json"]) end + pipeline :public_api_access do + plug(RetWeb.Plugs.RequirePublicApiAccess) + end + pipeline :proxy_api do plug(:accepts, ["json"]) plug(RetWeb.Plugs.RewriteAuthorizationHeaderToPerms) @@ -65,6 +69,11 @@ defmodule RetWeb.Router do plug(RetWeb.Plugs.RedirectToMainDomain) end + pipeline :graphql do + plug RetWeb.ApiTokenAuthPipeline + plug RetWeb.AddAbsintheContext + end + scope "/health", RetWeb do get("/", HealthController, :index) end @@ -112,10 +121,17 @@ defmodule RetWeb.Router do resources("/hubs", Api.V1.HubController, only: [:create, :delete]) end + # Must be defined before :show for scenes + scope "/v1", as: :api_v1 do + pipe_through([:auth_required]) + get("/scenes/projectless", Api.V1.SceneController, :index_projectless) + end + scope "/v1", as: :api_v1 do pipe_through([:auth_optional]) resources("/media/search", Api.V1.MediaSearchController, only: [:index]) resources("/avatars", Api.V1.AvatarController, only: [:show]) + resources("/scenes", Api.V1.SceneController, only: [:show]) end @@ -142,6 +158,24 @@ defmodule RetWeb.Router do end end + scope "/api/v2_alpha", RetWeb do + pipe_through( + [:secure_headers, :parsed_body, :api, :public_api_access, :auth_required] ++ + if(Mix.env() == :prod, do: [:ssl_only, :canonicalize_domain], else: []) + ) + + resources("/credentials", Api.V2.CredentialsController, only: [:create, :index, :update, :show]) + end + + scope "/api/v2_alpha", as: :api_v2_alpha do + pipe_through( + [:parsed_body, :api, :public_api_access, :graphql] ++ if(Mix.env() == :prod, do: [:ssl_only], else: []) + ) + + forward "/graphiql", Absinthe.Plug.GraphiQL, json_codec: Jason, schema: RetWeb.Schema + forward "/", Absinthe.Plug, json_codec: Jason, schema: RetWeb.Schema + end + # Directly accessible APIs. # Permit direct file uploads without intermediate ALB/Cloudfront/CDN proxying. scope "/api", RetWeb do diff --git a/lib/ret_web/schema.ex b/lib/ret_web/schema.ex new file mode 100644 index 000000000..fe4b8b770 --- /dev/null +++ b/lib/ret_web/schema.ex @@ -0,0 +1,35 @@ +defmodule RetWeb.Schema do + @moduledoc false + + use Absinthe.Schema + + import RetWeb.Middleware, only: [build_middleware: 3] + + def middleware(middleware, field, object) do + build_middleware(middleware, field, object) + end + + import_types(Absinthe.Type.Custom) + import_types(RetWeb.Schema.RoomTypes) + import_types(RetWeb.Schema.SceneTypes) + + query do + import_fields(:room_queries) + end + + mutation do + import_fields(:room_mutations) + end + + def context(ctx) do + loader = + Dataloader.new() + |> Dataloader.add_source(:db, Ret.Api.Dataloader.source()) + + Map.put(ctx, :loader, loader) + end + + def plugins do + [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() + end +end diff --git a/lib/ret_web/schema/room_types.ex b/lib/ret_web/schema/room_types.ex new file mode 100644 index 000000000..b4c6df379 --- /dev/null +++ b/lib/ret_web/schema/room_types.ex @@ -0,0 +1,235 @@ +defmodule RetWeb.Schema.RoomTypes do + @moduledoc "GraphQL Schema" + + use Absinthe.Schema.Notation + alias RetWeb.Resolvers + import_types(RetWeb.Schema.Types.Custom.JSON) + + @desc "Public TLS port number used for TURN" + object :turn_transport do + @desc "Public TLS port number used for TURN" + field(:port, :integer) + end + + @desc "TURN information for DLTS over TURN fallback, when enabled" + object :turn_info do + @desc "Cryptographic credential, good for two minutes" + field(:credential, :string) + @desc "Whether TURN is enabled/configured" + field(:enabled, :boolean) + @desc "List of public TLS ports" + field(:transports, list_of(:turn_transport)) + @desc "Username, good for two minutes" + field(:username, :string) + end + + @desc "Permissions for participants in the room" + input_object :input_member_permissions do + @desc "Allows non-admin participants to spawn and move media" + field(:spawn_and_move_media, :boolean) + @desc "Allows non-admin participants to spawn in-game cameras" + field(:spawn_camera, :boolean) + @desc "Allows non-admin participants to draw with a pen" + field(:spawn_drawing, :boolean) + @desc "Allows non-admin participants to pin media to the room" + field(:pin_objects, :boolean) + @desc "Allows non-admin participants to spawn emoji" + field(:spawn_emoji, :boolean) + @desc "Allows non-admin participants to toggle fly mode" + field(:fly, :boolean) + end + + @desc "Permissions for participants in the room" + object :member_permissions do + @desc "Allows non-admin participants to spawn and move media" + field(:spawn_and_move_media, :boolean) + @desc "Allows non-admin participants to spawn in-game cameras" + field(:spawn_camera, :boolean) + @desc "Allows non-admin participants to draw with a pen" + field(:spawn_drawing, :boolean) + @desc "Allows non-admin participants to pin media to the room" + field(:pin_objects, :boolean) + @desc "Allows non-admin participants to spawn emoji" + field(:spawn_emoji, :boolean) + @desc "Allows non-admin participants to toggle fly mode" + field(:fly, :boolean) + end + + @desc "A room" + object :room do + @desc "The room's unique ID" + field(:hub_sid, :id, name: "id") + @desc "The room's name" + field(:name, :string) + @desc "The room's name as it appears at the end of its URL" + field(:slug, :string) + @desc "A description of the room" + field(:description, :string) + @desc "Makes this room as public (while it is still open)" + field(:allow_promotion, :boolean) + @desc "Temporary entry code" + field(:entry_code, :string) + @desc "Determines if entry is allowed, denied, or by-invite-only. (Values are \"allow\", \"deny\", or \"invite\".)" + field(:entry_mode, :string) + @desc "The host server associated with this room via the load balancer" + field(:host, :string) + + @desc "The port number used to connect to the host server" + field(:port, :integer) do + @desc "The port number used to connect to the host server" + resolve(&Resolvers.RoomResolver.port/3) + end + + @desc "TURN information for DLTS over TURN fallback, when enabled" + field(:turn, :turn_info) do + resolve(&Resolvers.RoomResolver.turn/3) + end + + @desc """ + Can be used to remove the X-Frame-Options header that is usually served to the Hubs client when this room is loaded, so that the client can access this room from a ,