diff --git a/.gitignore b/.gitignore index 2fd9fd0f..a00f3c78 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ node_modules # this depending on your deployment strategy. /priv/static/ +# Temp files generated by tests +/apps/*/priv/test/ + # Mnesia DBs /apps/*/priv/mnesia* /priv/mnesia* diff --git a/apps/admin/lib/kaffy/editor_extension.ex b/apps/admin/lib/kaffy/editor_extension.ex index 140df393..2cea59a3 100644 --- a/apps/admin/lib/kaffy/editor_extension.ex +++ b/apps/admin/lib/kaffy/editor_extension.ex @@ -4,16 +4,19 @@ defmodule Legendary.Admin.Kaffy.EditorExtension do markdown editor library. """ + import Phoenix.HTML.Tag, only: [tag: 2] + def stylesheets(_conn) do [ - {:safe, ~s()}, + {:safe, ~s()}, {:safe, ~s()}, + tag(:meta, property: "og:site_name", content: Legendary.I18n.t!("en", "site.title")) ] end def javascripts(_conn) do [ - {:safe, ~s()}, + {:safe, ~s()}, {:safe, ~s()}, ] end diff --git a/apps/admin/mix.exs b/apps/admin/mix.exs index 99b841d9..ac482af8 100644 --- a/apps/admin/mix.exs +++ b/apps/admin/mix.exs @@ -12,7 +12,7 @@ defmodule Legendary.Admin.MixProject do config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: "~> 1.7", + elixir: "~> 1.10", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, diff --git a/apps/app/assets/css/admin.css b/apps/app/assets/css/admin.css new file mode 100644 index 00000000..e6b33558 --- /dev/null +++ b/apps/app/assets/css/admin.css @@ -0,0 +1,5 @@ +@import "content-editor-overrides"; + +.social-media-preview-image.social-media-preview-image { + border-radius: 16px !important; +} diff --git a/apps/app/assets/js/admin.js b/apps/app/assets/js/admin.js new file mode 100644 index 00000000..010f154c --- /dev/null +++ b/apps/app/assets/js/admin.js @@ -0,0 +1,4 @@ +import "../css/admin.css"; + +import "./admin/content-editor"; +import "./admin/preview-image"; diff --git a/apps/app/assets/js/content-editor.js b/apps/app/assets/js/admin/content-editor.js similarity index 90% rename from apps/app/assets/js/content-editor.js rename to apps/app/assets/js/admin/content-editor.js index 8add5194..9ec5d0c2 100644 --- a/apps/app/assets/js/content-editor.js +++ b/apps/app/assets/js/admin/content-editor.js @@ -1,7 +1,7 @@ -import { ready } from "./utils"; +import { ready } from "../utils"; import SimpleMDE from "simplemde"; import "simplemde/dist/simplemde.min.css"; -import "../css/content-editor-overrides.css"; +import "../../css/content-editor-overrides.css"; const requestPreview = (plainText, previewContainer) => { let request = new XMLHttpRequest(); diff --git a/apps/app/assets/js/admin/preview-image.js b/apps/app/assets/js/admin/preview-image.js new file mode 100644 index 00000000..4e46ebef --- /dev/null +++ b/apps/app/assets/js/admin/preview-image.js @@ -0,0 +1,140 @@ +import { ready } from "../utils"; +import { fabric } from "fabric"; + +fabric.Object.prototype.objectCaching = false; + +const textboxDefaults = { + lockMovementX: true, + lockMovementY: true, + lockScalingX: true, + lockScalingY: true, + lockSkewingX: true, + lockSkewingY: true, + lockRotation: true, + lockUniScaling: true, + hasControls: false, + selectable: true, + fontFamily: + "system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'", +}; + +const updateDataURL = (canvas) => { + canvas.renderAll(); + const data = canvas.toDataURL({ + enableRetinaScaling: true, + }); + canvas.dataURLInput.value = data; +}; + +const setTextboxValue = (canvas, textbox, value) => { + textbox.set({ text: value }); + updateDataURL(canvas); +}; + +const setTextboxFromEvent = (canvas, textbox, { target }) => { + setTextboxValue(canvas, textbox, target.value); +}; + +const makeLinkedTextbox = (canvas, selector, opts) => { + const box = new fabric.Textbox("", { + ...textboxDefaults, + ...opts, + }); + box.on("input", updateDataURL.bind(box, canvas)); + + var input = document.querySelector(selector); + + canvas.add(box); + + setTextboxValue(canvas, box, input.value); + input.addEventListener("input", setTextboxFromEvent.bind(input, canvas, box)); + + return box; +}; + +const makeStaticTextbox = (canvas, value, opts) => { + const box = new fabric.Textbox(value, { + ...textboxDefaults, + ...opts, + }); + box.on("input", updateDataURL.bind(box, canvas)); + + canvas.add(box); + + return box; +}; + +const prepareCanvas = (input, canvasElement) => { + const inputContainer = input.parentElement; + + input.setAttribute("type", "hidden"); + + canvasElement.setAttribute( + "class", + "social-media-preview-image rounded-lg border-2 border-gray-300" + ); + canvasElement.setAttribute("width", 800); + canvasElement.setAttribute("height", 418); + inputContainer.appendChild(canvasElement); + + const canvas = new fabric.Canvas(canvasElement); + canvas.dataURLInput = input; + + return canvas; +}; + +ready(() => { + const input = document.querySelector( + "[name='post[social_media_preview_image]']" + ); + const canvasElement = document.createElement("canvas"); + const canvas = prepareCanvas(input, canvasElement); + + fabric.Image.fromURL( + "/images/social-media-preview-background.png", + function (oImg) { + oImg.selectable = false; + canvas.add(oImg); + + const title = makeLinkedTextbox(canvas, "[name='post[title]']", { + left: 80, + top: 80, + width: 640, + fontWeight: "bold", + fontSize: 36, + }); + + makeLinkedTextbox(canvas, "[name='post[excerpt]']", { + left: 80, + width: 560, + top: title.aCoords.bl.y + 20, + fill: "#4B5563", + fontSize: 18, + }); + + var name = document + .querySelector("[property='og:site_name']") + .getAttribute("content"); + makeStaticTextbox(canvas, name, { + left: 80, + width: 560, + top: 48, + fill: "#F87171", + fontSize: 18, + fontWeight: "bold", + }); + + var rect = new fabric.Rect({ + left: 0, + top: 0, + fill: "#F87171", + width: 14, + height: 418, + selectable: false, + }); + canvas.add(rect); + + updateDataURL(canvas); + } + ); +}); diff --git a/apps/app/assets/package-lock.json b/apps/app/assets/package-lock.json index 58f88472..92f7ee65 100644 --- a/apps/app/assets/package-lock.json +++ b/apps/app/assets/package-lock.json @@ -10,6 +10,7 @@ "@fortawesome/fontawesome-free": "^5.14.0", "alpinejs": "^2.8.1", "autoprefixer": "^9.8.6", + "fabric": "^4.6.0", "glob": "^7.1.6", "npm-force-resolutions": "^0.0.10", "phoenix": "file:/../../../deps/phoenix", @@ -82,6 +83,18 @@ "@webassemblyjs/utf8": "1.11.0" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pngquant-bin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngquant-bin/-/pngquant-bin-6.0.0.tgz", @@ -129,6 +142,25 @@ "node": ">=0.10.0" } }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "optional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "optional": true, + "dependencies": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -266,6 +298,22 @@ "url": "https://opencollective.com/postcss/" } }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "optional": true, + "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" + } + }, "node_modules/mozjpeg": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/mozjpeg/-/mozjpeg-7.1.0.tgz", @@ -377,6 +425,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", @@ -389,6 +446,12 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "optional": true + }, "node_modules/node-emoji": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", @@ -411,6 +474,12 @@ "node": ">=4" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", @@ -446,7 +515,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, + "devOptional": true, "dependencies": { "semver": "^6.0.0" }, @@ -631,6 +700,21 @@ "node": ">=0.6.0" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/webpack-cli/node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -640,6 +724,12 @@ "node": ">=8" } }, + "node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "optional": true + }, "node_modules/css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -674,6 +764,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "node_modules/postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", @@ -797,6 +897,18 @@ "node": ">=0.10.0" } }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/postcss-modules-values/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1055,6 +1167,18 @@ "node": ">=0.10.0" } }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", @@ -1180,6 +1304,18 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/less/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -1190,6 +1326,15 @@ "semver": "bin/semver" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1215,6 +1360,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1367,6 +1518,18 @@ "node": ">= 0.4" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/squeak/node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -1393,6 +1556,15 @@ "node": ">=8" } }, + "node_modules/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==", + "optional": true, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/postcss-sorting": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-5.0.1.tgz", @@ -1477,6 +1649,44 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "optional": true, + "dependencies": { + "isstream": "~0.1.2", + "oauth-sign": "~0.9.0", + "safe-buffer": "^5.1.2", + "is-typedarray": "~1.0.0", + "json-stringify-safe": "~5.0.1", + "performance-now": "^2.1.0", + "http-signature": "~1.2.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2", + "har-validator": "~5.1.3", + "extend": "~3.0.2", + "mime-types": "~2.1.19", + "tough-cookie": "~2.5.0", + "aws-sign2": "~0.7.0", + "caseless": "~0.12.0", + "aws4": "^1.8.0", + "forever-agent": "~0.6.1", + "combined-stream": "~1.0.6", + "form-data": "~2.3.2", + "qs": "~6.5.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/decompress-unzip/node_modules/get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", @@ -1496,6 +1706,19 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -1527,6 +1750,51 @@ "node": ">=8.12.0" } }, + "node_modules/jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "optional": true, + "dependencies": { + "acorn-globals": "^4.3.2", + "xml-name-validator": "^3.0.0", + "data-urls": "^1.1.0", + "parse5": "5.1.0", + "nwsapi": "^2.2.0", + "acorn": "^7.1.0", + "html-encoding-sniffer": "^1.0.2", + "whatwg-mimetype": "^2.3.0", + "cssstyle": "^2.0.0", + "symbol-tree": "^3.2.2", + "w3c-hr-time": "^1.0.1", + "saxes": "^3.1.9", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "webidl-conversions": "^4.0.2", + "pn": "^1.1.0", + "whatwg-encoding": "^1.0.5", + "escodegen": "^1.11.1", + "array-equal": "^1.0.0", + "tough-cookie": "^3.0.1", + "w3c-xmlserializer": "^1.1.2", + "whatwg-url": "^7.0.0", + "domexception": "^1.0.1", + "cssom": "^0.4.1", + "ws": "^7.0.0", + "abab": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/pngquant-bin/node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1570,6 +1838,15 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-bigint": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", @@ -1663,6 +1940,17 @@ "node": ">=6" } }, + "node_modules/simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -1683,6 +1971,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "optional": true, + "engines": { + "node": ">=4" + } + }, "node_modules/domhandler": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", @@ -1700,6 +1997,21 @@ "node": ">=0.10.0" } }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -1766,6 +2078,12 @@ "node": ">= 8" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true + }, "node_modules/colorette": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", @@ -1775,7 +2093,7 @@ "version": "2.1.31", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", - "dev": true, + "devOptional": true, "dependencies": { "mime-db": "1.48.0" }, @@ -1791,6 +2109,15 @@ "ms": "^2.1.1" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/postcss-css-variables/node_modules/postcss": { "version": "6.0.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", @@ -2016,6 +2343,18 @@ "node": ">=8" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2040,17 +2379,27 @@ "dev": true, "optional": true }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "devOptional": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "devOptional": true }, "node_modules/@babel/types": { "version": "7.14.5", @@ -2422,6 +2771,12 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/postcss-modules-extract-imports": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", @@ -2498,6 +2853,18 @@ "node": "*" } }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "optional": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.3.760", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.760.tgz", @@ -2520,6 +2887,21 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "node_modules/@babel/helper-replace-supers": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", @@ -2616,7 +2998,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=4.0" } @@ -2625,7 +3007,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -2905,7 +3287,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, + "devOptional": true, "dependencies": { "ms": "2.1.2" }, @@ -2964,6 +3346,26 @@ "node": ">=8" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -3030,6 +3432,21 @@ "node": ">=6.9.0" } }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/file-loader/node_modules/schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", @@ -3067,6 +3484,12 @@ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", "dev": true }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "optional": true + }, "node_modules/optipng-bin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-7.0.0.tgz", @@ -3181,6 +3604,24 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -3191,6 +3632,24 @@ "node": ">=6" } }, + "node_modules/request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "optional": true, + "dependencies": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, "node_modules/got": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", @@ -3364,6 +3823,12 @@ "node": "*" } }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "optional": true + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -3491,6 +3956,15 @@ "node": ">= 4" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3512,6 +3986,15 @@ "node": ">=8" } }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -3575,6 +4058,19 @@ "node": ">=4.0.0" } }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "optional": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -3854,7 +4350,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-regex": "^5.0.0" }, @@ -3943,6 +4439,12 @@ "node": ">=6.9.0" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, "node_modules/css-color-function": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/css-color-function/-/css-color-function-1.3.3.tgz", @@ -3971,6 +4473,18 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "optional": true, + "dependencies": { + "xmlchars": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sugarss/node_modules/postcss": { "version": "7.0.36", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", @@ -4085,6 +4599,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "optional": true, + "dependencies": { + "iconv-lite": "0.4.24" + } + }, "node_modules/trough": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", @@ -4185,6 +4708,18 @@ "node": ">=0.10.0" } }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss-colormin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.0.tgz", @@ -4203,6 +4738,15 @@ "postcss": "^8.2.15" } }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4370,6 +4914,12 @@ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "optional": true + }, "node_modules/autoprefixer/node_modules/postcss": { "version": "7.0.36", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", @@ -4533,7 +5083,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "devOptional": true }, "node_modules/p-timeout": { "version": "1.2.1", @@ -4747,7 +5297,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4774,6 +5324,12 @@ "postcss": "^8.2.15" } }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "optional": true + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -4846,6 +5402,19 @@ "dev": true, "optional": true }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/glob-parent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.0.tgz", @@ -4899,6 +5468,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "optional": true + }, "node_modules/stylelint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4936,6 +5511,12 @@ "node": ">= 4" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -5023,6 +5604,18 @@ "node": ">=0.10.0" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/log-symbols/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5089,7 +5682,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "devOptional": true }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.0", @@ -5261,7 +5854,7 @@ "version": "1.48.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 0.6" } @@ -5289,11 +5882,17 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "optional": true + }, "node_modules/log-symbols/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5374,6 +5973,20 @@ "node": ">=12" } }, + "node_modules/tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "optional": true, + "dependencies": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/postcss-color-function/node_modules/postcss": { "version": "6.0.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", @@ -5395,6 +6008,15 @@ "node": ">= 6" } }, + "node_modules/domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "optional": true, + "dependencies": { + "webidl-conversions": "^4.0.2" + } + }, "node_modules/postcss-normalize-url": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", @@ -5523,6 +6145,15 @@ "node": ">=6" } }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ], + "optional": true + }, "node_modules/table/node_modules/ajv": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", @@ -5554,6 +6185,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -5698,6 +6342,12 @@ "node": ">=4" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "optional": true + }, "node_modules/postcss-scss/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5785,6 +6435,12 @@ "node": ">=6" } }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/imagemin-pngquant/node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", @@ -5873,6 +6529,16 @@ "node": "*" } }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/style-loader/node_modules/loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -5904,7 +6570,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, "optional": true }, "node_modules/through": { @@ -6143,6 +6808,36 @@ "color-name": "1.1.3" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", @@ -6197,6 +6892,31 @@ "node": ">=6" } }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regenerate-unicode-properties": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", @@ -6425,6 +7145,12 @@ "integrity": "sha512-f8kcHX1ArhllUtb/wVSyvygoKCznIjnxhLxy7TCvIiMdT7fL4ZDTIKaadMe6eLvOXg6Wk02UeoFgUoZ2EKZZUA==", "dev": true }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, "node_modules/log-symbols/node_modules/chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -6519,6 +7245,18 @@ "node": ">=6" } }, + "node_modules/array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "optional": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, "node_modules/bin-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", @@ -6768,6 +7506,21 @@ "node": ">= 8" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -6945,6 +7698,12 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true + }, "node_modules/css-declaration-sorter": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz", @@ -7010,6 +7769,15 @@ "node": ">=0.4.0" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/camelcase-keys": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", @@ -7056,11 +7824,23 @@ "yallist": "^2.1.2" } }, + "node_modules/fabric": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-4.6.0.tgz", + "integrity": "sha512-MhJXCD/ZugOGV5aPHIG0MY1q2EfrlzC2sasrAHj0HHXN50JTe1bHFrlRdkXBijCJ0dG81fGu/A/Pct9DyuwCzQ==", + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "canvas": "^2.6.1", + "jsdom": "^15.2.1" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "dependencies": { "punycode": "^2.1.0" } @@ -7119,6 +7899,15 @@ "node": ">=4" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -7177,6 +7966,15 @@ "node": ">=0.10.0" } }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/camelcase-keys/node_modules/map-obj": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", @@ -7231,6 +8029,19 @@ "node": ">=0.10.0" } }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -7353,6 +8164,30 @@ "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/webpack": { "version": "5.41.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.41.0.tgz", @@ -7500,6 +8335,12 @@ "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" }, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "optional": true + }, "node_modules/download/node_modules/pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -7535,7 +8376,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, + "devOptional": true, "bin": { "semver": "bin/semver.js" } @@ -7566,6 +8407,17 @@ "dev": true, "optional": true }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "optional": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/object.getownpropertydescriptors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", @@ -7756,6 +8608,20 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "optional": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -7822,7 +8688,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -7904,6 +8770,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -8028,6 +8903,18 @@ "node": ">=6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8369,6 +9256,12 @@ "node": ">=4" } }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "optional": true + }, "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", @@ -8391,7 +9284,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "optional": true, "bin": { "esparse": "bin/esparse.js", @@ -8432,7 +9324,7 @@ } }, "../../../deps/phoenix": { - "version": "1.6.0", + "version": "1.6.2", "license": "MIT" }, "node_modules/esrecurse": { @@ -8600,6 +9492,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "optional": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8789,6 +9687,17 @@ "webpack": "^4.4.0 || ^5.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "optional": true, + "dependencies": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, "node_modules/buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -8921,6 +9830,12 @@ "node": ">=0.10.0" } }, + "node_modules/nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "optional": true + }, "node_modules/@nodelib/fs.walk": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", @@ -8947,6 +9862,21 @@ "seek-table": "bin/seek-bzip-table" } }, + "node_modules/canvas": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", + "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.14.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -8962,7 +9892,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "devOptional": true }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.14.5", @@ -9162,6 +10092,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, "node_modules/tar-stream/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -9369,6 +10305,20 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "node_modules/gauge/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hosted-git-info/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -9395,7 +10345,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "devOptional": true }, "node_modules/find-versions": { "version": "3.2.0", @@ -9474,6 +10424,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "optional": true + }, "node_modules/meow/node_modules/type-fest": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", @@ -9492,6 +10448,18 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -9501,6 +10469,21 @@ "node": ">=0.10" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/unified/node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -9578,6 +10561,15 @@ "resolved": "../../../deps/phoenix_live_view", "link": true }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "optional": true, + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -9613,6 +10605,21 @@ "postcss": "^8.2.15" } }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -9646,6 +10653,26 @@ "node": ">=0.10.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, "node_modules/postcss-normalize-charset": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", @@ -9662,7 +10689,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "optional": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -9944,6 +10970,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "optional": true + }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", @@ -10084,6 +11116,15 @@ "node": ">=8.6" } }, + "node_modules/are-we-there-yet/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -10172,6 +11213,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/request-promise-native/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/unicode-match-property-value-ecmascript": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", @@ -10252,6 +11306,12 @@ "node": ">= 0.10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "optional": true + }, "node_modules/console-stream": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", @@ -10315,11 +11375,22 @@ "node": ">= 8" } }, + "node_modules/data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "optional": true, + "dependencies": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -10378,6 +11449,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true + }, "node_modules/cwebp-bin": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cwebp-bin/-/cwebp-bin-5.1.0.tgz", @@ -10421,6 +11498,12 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "optional": true + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -10434,7 +11517,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "devOptional": true }, "node_modules/postcss-less/node_modules/supports-color": { "version": "6.1.0", @@ -10468,11 +11551,16 @@ "node": ">=0.10.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "optional": true }, "node_modules/imagemin-mozjpeg/node_modules/npm-run-path": { @@ -10544,6 +11632,23 @@ "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==", "dev": true }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", @@ -10608,6 +11713,15 @@ "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=" }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -10742,6 +11856,12 @@ "node": ">=6.9.0" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -10817,6 +11937,20 @@ "webpack": "^5.0.0" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/style-loader": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", @@ -10853,6 +11987,33 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", @@ -10910,6 +12071,15 @@ "@webassemblyjs/helper-wasm-bytecode": "1.11.0" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/unicode-property-aliases-ecmascript": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", @@ -10999,7 +12169,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -11069,6 +12239,28 @@ "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/is-jpg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", @@ -11359,6 +12551,20 @@ "node": ">=4" } }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -11386,6 +12592,12 @@ "node": ">=8.0.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -11416,7 +12628,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, "optional": true }, "node_modules/object-inspect": { @@ -11533,6 +12744,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -11689,6 +12906,15 @@ "postcss": "^8.1.13" } }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/decompress-targz": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", @@ -11802,6 +13028,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -11815,6 +13058,15 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "optional": true, + "dependencies": { + "whatwg-encoding": "^1.0.1" + } + }, "node_modules/css-unit-converter": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", @@ -11858,6 +13110,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@babel/helper-function-name": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", @@ -11902,6 +13166,15 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/imagemin-mozjpeg/node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -11942,6 +13215,27 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "optional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/got/node_modules/get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -12033,6 +13327,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "optional": true + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -12070,6 +13370,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12094,6 +13403,15 @@ "postcss": "^8.2.15" } }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/bl/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13180,6 +14498,58 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -13500,11 +14870,47 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "optional": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "optional": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "optional": true + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "optional": true + } + } + }, "acorn-node": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", @@ -13520,11 +14926,20 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13553,7 +14968,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "devOptional": true }, "ansi-styles": { "version": "3.2.1", @@ -13572,6 +14987,12 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "optional": true + }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -13598,6 +15019,42 @@ } } }, + "are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "arg": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.0.tgz", @@ -13613,6 +15070,12 @@ "sprintf-js": "~1.0.2" } }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "optional": true + }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -13625,12 +15088,33 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "optional": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, "autoprefixer": { "version": "9.8.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", @@ -13670,6 +15154,18 @@ } } }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "optional": true + }, "babel-loader": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", @@ -13739,6 +15235,15 @@ "dev": true, "optional": true }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -13983,6 +15488,12 @@ "fill-range": "^7.0.1" } }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "optional": true + }, "browserslist": { "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", @@ -14110,6 +15621,23 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001240.tgz", "integrity": "sha512-nb8mDzfMdxBDN7ZKx8chWafAdBp5DAAlpWvNyUGe5tcDWd838zpzDN3Rah9cjCqhfOKkrvx40G2SDtP0qiWX/w==" }, + "canvas": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", + "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "optional": true, + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.14.0", + "simple-get": "^3.0.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, "caw": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", @@ -14176,6 +15704,12 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -14235,6 +15769,12 @@ "q": "^1.1.2" } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true + }, "codemirror": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.0.tgz", @@ -14297,6 +15837,15 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -14325,6 +15874,12 @@ "proto-list": "~1.2.1" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true + }, "console-stream": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", @@ -14408,7 +15963,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, "optional": true }, "cosmiconfig": { @@ -14697,6 +16251,29 @@ } } }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "optional": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "optional": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "optional": true + } + } + }, "cwebp-bin": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cwebp-bin/-/cwebp-bin-5.1.0.tgz", @@ -14709,11 +16286,31 @@ "logalot": "^2.1.0" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "optional": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, + "devOptional": true, "requires": { "ms": "2.1.2" } @@ -14899,6 +16496,12 @@ } } }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -14913,6 +16516,24 @@ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "optional": true + }, "detective": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", @@ -14972,6 +16593,15 @@ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "dev": true }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "optional": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, "domhandler": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", @@ -15051,6 +16681,16 @@ "dev": true, "optional": true }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "electron-to-chromium": { "version": "1.3.760", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.760.tgz", @@ -15178,6 +16818,27 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -15192,7 +16853,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "optional": true }, "esrecurse": { @@ -15216,13 +16876,13 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "devOptional": true }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "devOptional": true }, "events": { "version": "3.3.0", @@ -15362,13 +17022,28 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "devOptional": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "optional": true + }, + "fabric": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-4.6.0.tgz", + "integrity": "sha512-MhJXCD/ZugOGV5aPHIG0MY1q2EfrlzC2sasrAHj0HHXN50JTe1bHFrlRdkXBijCJ0dG81fGu/A/Pct9DyuwCzQ==", + "requires": { + "canvas": "^2.6.1", + "jsdom": "^15.2.1" + } }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "devOptional": true }, "fast-glob": { "version": "3.2.7", @@ -15396,7 +17071,13 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "devOptional": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "optional": true }, "fast-xml-parser": { "version": "3.19.0", @@ -15574,6 +17255,23 @@ "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", "dev": true }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -15598,6 +17296,15 @@ } } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -15614,6 +17321,59 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -15656,6 +17416,15 @@ "pump": "^3.0.0" } }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "gifsicle": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-5.2.0.tgz", @@ -15878,6 +17647,22 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "optional": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, "hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -15942,6 +17727,12 @@ "has-symbol-support-x": "^1.4.1" } }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true + }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -15986,6 +17777,15 @@ "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", "dev": true }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "optional": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, "html-tags": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", @@ -16005,12 +17805,42 @@ "readable-stream": "^3.1.1" } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "icss-utils": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", @@ -16499,6 +18329,12 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "optional": true + }, "is-absolute-url": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", @@ -16640,7 +18476,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "devOptional": true }, "is-gif": { "version": "3.0.0", @@ -16805,7 +18641,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "devOptional": true }, "is-unicode-supported": { "version": "0.1.0", @@ -16823,7 +18659,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, "optional": true }, "isexe": { @@ -16838,6 +18673,12 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, "isurl": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", @@ -16893,6 +18734,46 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "optional": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -16915,11 +18796,23 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "optional": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "devOptional": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true }, "json5": { "version": "2.2.0", @@ -16946,6 +18839,18 @@ } } }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "junk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", @@ -17044,6 +18949,16 @@ } } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "lilconfig": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", @@ -17114,6 +19029,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "optional": true + }, "lodash.toarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", @@ -17303,7 +19224,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, + "devOptional": true, "requires": { "semver": "^6.0.0" } @@ -17432,13 +19353,13 @@ "version": "1.48.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", - "dev": true + "devOptional": true }, "mime-types": { "version": "2.1.31", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", - "dev": true, + "devOptional": true, "requires": { "mime-db": "1.48.0" } @@ -17521,6 +19442,41 @@ "kind-of": "^6.0.3" } }, + "minipass": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz", + "integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + } + } + }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -17553,6 +19509,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "optional": true + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -17586,11 +19548,26 @@ "lodash.toarray": "^4.4.0" } }, + "node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==", + "optional": true + }, "node-releases": { "version": "1.1.73", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "requires": { + "abbrev": "1" + } + }, "normalize-package-data": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", @@ -17685,6 +19662,18 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -17699,11 +19688,29 @@ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "optional": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "devOptional": true }, "object-hash": { "version": "2.2.0", @@ -17776,6 +19783,20 @@ "mimic-fn": "^2.1.0" } }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "optipng-bin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-7.0.0.tgz", @@ -17933,6 +19954,12 @@ "lines-and-columns": "^1.1.6" } }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "optional": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -17968,6 +19995,12 @@ "dev": true, "optional": true }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, "phoenix": { "version": "file:../../../deps/phoenix" }, @@ -18015,6 +20048,12 @@ "find-up": "^4.0.0" } }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "optional": true + }, "pngquant-bin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngquant-bin/-/pngquant-bin-6.0.0.tgz", @@ -18978,6 +21017,12 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "optional": true + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -19000,7 +21045,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "optional": true }, "proto-list": { @@ -19023,6 +21067,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "optional": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -19038,7 +21088,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "devOptional": true }, "purgecss": { "version": "4.0.3", @@ -19065,6 +21115,12 @@ "dev": true, "optional": true }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "optional": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -19337,6 +21393,84 @@ "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "optional": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "optional": true + } + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "optional": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "optional": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -19418,7 +21552,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "devOptional": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true }, "sass": { "version": "1.35.1", @@ -19449,6 +21589,15 @@ "dev": true, "optional": true }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "optional": true, + "requires": { + "xmlchars": "^2.1.1" + } + }, "schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", @@ -19474,7 +21623,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "devOptional": true }, "semver-regex": { "version": "2.0.0", @@ -19511,6 +21660,12 @@ "randombytes": "^2.1.0" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -19541,7 +21696,41 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "devOptional": true + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "optional": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true + } + } }, "simple-swizzle": { "version": "0.2.2", @@ -19756,12 +21945,35 @@ } } }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "dev": true }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "optional": true + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -19816,7 +22028,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, + "devOptional": true, "requires": { "ansi-regex": "^5.0.0" } @@ -20223,6 +22435,12 @@ "util.promisify": "~1.0.0" } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "optional": true + }, "table": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", @@ -20373,6 +22591,34 @@ "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", "dev": true }, + "tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "optional": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + } + } + }, "tar-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", @@ -20550,6 +22796,26 @@ "resolved": "https://registry.npmjs.org/topbar/-/topbar-1.0.1.tgz", "integrity": "sha512-HZqQSMBiG29vcjOrqKCM9iGY/h69G5gQH7ae83ZCPz5uPmbQKwK0sMEqzVDBiu64tWHJ+kk9NApECrF+FAAvRA==" }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "optional": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "optional": true, + "requires": { + "punycode": "^2.1.0" + } + }, "trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -20582,12 +22848,26 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "optional": true, "requires": { "safe-buffer": "^5.0.1" } }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "optional": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-fest": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", @@ -20729,7 +23009,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "requires": { "punycode": "^2.1.0" } @@ -20797,6 +23077,17 @@ "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", "dev": true }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "vfile": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", @@ -20819,6 +23110,26 @@ "unist-util-stringify-position": "^2.0.0" } }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "optional": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "optional": true, + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, "watchpack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", @@ -20836,6 +23147,12 @@ } } }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "optional": true + }, "webpack": { "version": "5.41.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.41.0.tgz", @@ -21037,6 +23354,32 @@ } } }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "optional": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "optional": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "optional": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -21060,12 +23403,39 @@ "is-symbol": "^1.0.3" } }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "optional": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "optional": true + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -21083,6 +23453,24 @@ "typedarray-to-buffer": "^3.1.5" } }, + "ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "optional": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "optional": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "optional": true + }, "xmlhttprequest": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", diff --git a/apps/app/assets/package.json b/apps/app/assets/package.json index f74e00f1..c3af56a8 100644 --- a/apps/app/assets/package.json +++ b/apps/app/assets/package.json @@ -12,6 +12,7 @@ "@fortawesome/fontawesome-free": "^5.14.0", "alpinejs": "^2.8.1", "autoprefixer": "^9.8.6", + "fabric": "^4.6.0", "glob": "^7.1.6", "npm-force-resolutions": "^0.0.10", "phoenix": "file:/../../../deps/phoenix", diff --git a/apps/app/assets/static/images/social-media-preview-background.png b/apps/app/assets/static/images/social-media-preview-background.png new file mode 100644 index 00000000..9f3c8fc8 Binary files /dev/null and b/apps/app/assets/static/images/social-media-preview-background.png differ diff --git a/apps/app/assets/webpack.config.js b/apps/app/assets/webpack.config.js index 6196188a..f928ffb0 100644 --- a/apps/app/assets/webpack.config.js +++ b/apps/app/assets/webpack.config.js @@ -21,7 +21,7 @@ module.exports = (env, options) => { devtool: devMode ? "source-map" : undefined, entry: { app: glob.sync("./vendor/**/*.js").concat(["./js/app.js"]), - "content-editor": ["./js/content-editor.js"], + admin: ["./js/admin.js"], }, output: { filename: "js/[name].js", diff --git a/apps/app/lib/app_web/endpoint.ex b/apps/app/lib/app_web/endpoint.ex index cf76eed1..e0069ce0 100644 --- a/apps/app/lib/app_web/endpoint.ex +++ b/apps/app/lib/app_web/endpoint.ex @@ -31,7 +31,7 @@ defmodule AppWeb.Endpoint do at: "/", from: :app, gzip: false, - only: ~w(css fonts images js favicon.ico robots.txt) + only: ~w(css fonts images js favicon.ico robots.txt public_uploads) plug Plug.Static, at: "/kaffy", diff --git a/apps/app/lib/app_web/router.ex b/apps/app/lib/app_web/router.ex index fbb3a9cf..391c2595 100644 --- a/apps/app/lib/app_web/router.ex +++ b/apps/app/lib/app_web/router.ex @@ -64,5 +64,6 @@ defmodule AppWeb.Router do use Legendary.Core.Routes use Legendary.Admin.Routes + use Legendary.ObjectStorageWeb.Routes use Legendary.Content.Routes end diff --git a/apps/app/lib/app_web/templates/layout/_social.html.eex b/apps/app/lib/app_web/templates/layout/_social.html.eex index 52b67318..374ba7ff 100644 --- a/apps/app/lib/app_web/templates/layout/_social.html.eex +++ b/apps/app/lib/app_web/templates/layout/_social.html.eex @@ -23,6 +23,4 @@ - - - +<%= preview_image_tags(@conn, assigns) %> diff --git a/apps/app/lib/app_web/views/layout_view.ex b/apps/app/lib/app_web/views/layout_view.ex index d9d942e8..911ef70b 100644 --- a/apps/app/lib/app_web/views/layout_view.ex +++ b/apps/app/lib/app_web/views/layout_view.ex @@ -1,6 +1,8 @@ defmodule AppWeb.LayoutView do use AppWeb, :view + alias Legendary.ContentWeb.Uploaders.SocialMediaPreview + def title(conn, assigns), do: title(view_module(conn), view_template(conn), assigns) def title(view, template, %{post: post}), do: "#{post.title} | #{title(view, template, nil)}" @@ -54,4 +56,18 @@ defmodule AppWeb.LayoutView do def published_tag(_, _, _) do nil end + + def preview_image_tags(_conn, %{post: post}) do + url = SocialMediaPreview.url({"original.png", post}, :original) + + [ + tag(:meta, itemprop: "image", content: url), + tag(:meta, name: "twitter:image:src", content: url), + tag(:meta, property: "og:image", content: url) + ] + end + + def preview_image_tags(_, _) do + nil + end end diff --git a/apps/app/mix.exs b/apps/app/mix.exs index a112c6ec..50602ad9 100644 --- a/apps/app/mix.exs +++ b/apps/app/mix.exs @@ -9,7 +9,7 @@ defmodule App.MixProject do config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: "~> 1.7", + elixir: "~> 1.10", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, @@ -42,6 +42,7 @@ defmodule App.MixProject do {:admin, in_umbrella: true}, {:content, in_umbrella: true}, {:core, in_umbrella: true}, + {:object_storage, in_umbrella: true}, {:ecto_sql, "~> 3.7"}, {:excoveralls, "~> 0.10", only: [:dev, :test]}, {:floki, ">= 0.30.0"}, diff --git a/apps/app/test/app_web/views/layout_view_test.exs b/apps/app/test/app_web/views/layout_view_test.exs index ca03e06b..34c23c50 100644 --- a/apps/app/test/app_web/views/layout_view_test.exs +++ b/apps/app/test/app_web/views/layout_view_test.exs @@ -7,6 +7,7 @@ defmodule App.LayoutViewTest do alias Legendary.Content.Post @post %Post{ + name: "test-slug", title: "Test Post", excerpt: "This is a test post.", modified_gmt: ~N[2021-09-17T00:00:00], @@ -66,4 +67,19 @@ defmodule App.LayoutViewTest do assert published_tag(nil, nil, nil) == nil end end + + describe "preview_image_tags/2" do + test "for a post" do + markup = + preview_image_tags(nil, %{post: @post}) + |> Enum.map(&safe_to_string/1) + |> Enum.join("") + + assert markup =~ "/public_uploads/content/posts/preview_images/test-slug/original.png" + end + + test "without a post" do + assert preview_image_tags(nil, nil) == nil + end + end end diff --git a/apps/content/lib/content/post_admin.ex b/apps/content/lib/content/post_admin.ex index c846c1e2..3f7d4f25 100644 --- a/apps/content/lib/content/post_admin.ex +++ b/apps/content/lib/content/post_admin.ex @@ -3,6 +3,8 @@ defmodule Legendary.Content.PostAdmin do Custom admin logic for content posts and pages. """ + alias Legendary.Content.{Post, Posts.PreviewImages} + import Ecto.Query, only: [from: 2] def singular_name(_) do @@ -14,11 +16,15 @@ defmodule Legendary.Content.PostAdmin do end def create_changeset(schema, attrs) do - Legendary.Content.Post.changeset(schema, attrs) + schema + |> Post.changeset(attrs) + |> PreviewImages.handle_preview_image_upload(attrs) end def update_changeset(schema, attrs) do - Legendary.Content.Post.changeset(schema, attrs) + schema + |> Post.changeset(attrs) + |> PreviewImages.handle_preview_image_upload(attrs) end def index(_) do @@ -58,6 +64,7 @@ defmodule Legendary.Content.PostAdmin do comment_status: %{choices: [{"open", :open}, {"closed", :closed}]}, ping_status: %{choices: [{"open", :open}, {"closed", :closed}]}, menu_order: nil, + social_media_preview_image: %{type: :hidden}, ] end end diff --git a/apps/content/lib/content/posts/preview_images.ex b/apps/content/lib/content/posts/preview_images.ex new file mode 100644 index 00000000..926eee26 --- /dev/null +++ b/apps/content/lib/content/posts/preview_images.ex @@ -0,0 +1,30 @@ +defmodule Legendary.Content.Posts.PreviewImages do + @moduledoc """ + Handles storing social media preview images which are submitted as data uris + in the social_media_preview_image field. + """ + alias Ecto.Changeset + alias Legendary.ContentWeb.Uploaders.SocialMediaPreview + alias Legendary.CoreWeb.Base64Uploads + + def handle_preview_image_upload(changeset, attrs) do + upload = + case attrs do + %{"social_media_preview_image" => data} when is_binary(data) -> + Base64Uploads.data_uri_to_upload(data) + _ -> nil + end + + case upload do + nil -> + changeset + %Plug.Upload{} -> + name = Changeset.get_field(changeset, :name) + {:ok, _filename} = SocialMediaPreview.store({upload, %{name: name}}) + changeset + :error -> + changeset + |> Changeset.add_error(:social_media_preview_image, "is malformed") + end + end +end diff --git a/apps/content/lib/content_web/uploaders/social_media_preview.ex b/apps/content/lib/content_web/uploaders/social_media_preview.ex new file mode 100644 index 00000000..9247cddf --- /dev/null +++ b/apps/content/lib/content_web/uploaders/social_media_preview.ex @@ -0,0 +1,18 @@ +defmodule Legendary.ContentWeb.Uploaders.SocialMediaPreview do + @moduledoc """ + Uploader definition for social media preview images. + """ + use Waffle.Definition + + @versions [:original] + + # Override the persisted filenames: + def filename(version, _) do + Atom.to_string(version) + end + + # Override the storage directory: + def storage_dir(_version, {_file, %{name: name}}) do + "public_uploads/content/posts/preview_images/#{name}" + end +end diff --git a/apps/content/mix.exs b/apps/content/mix.exs index 4d0aef56..c884e62d 100644 --- a/apps/content/mix.exs +++ b/apps/content/mix.exs @@ -11,7 +11,7 @@ defmodule Legendary.Content.MixProject do config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: "~> 1.7", + elixir: "~> 1.10", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, diff --git a/apps/content/test/content/posts/preview_images_test.exs b/apps/content/test/content/posts/preview_images_test.exs new file mode 100644 index 00000000..67253f5e --- /dev/null +++ b/apps/content/test/content/posts/preview_images_test.exs @@ -0,0 +1,36 @@ +defmodule Legendary.Content.Posts.PreviewImagesTest do + use Legendary.Content.DataCase + + import Legendary.Content.Posts.PreviewImages + + alias Legendary.Content.Post + + @png "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAMJlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAARAAAAcgEyAAIAAAAUAAAAhIdpAAQAAAABAAAAmAAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciAzLjkuOAAAMjAyMTowOToyMiAxNTowOTowMQAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAAYjzhKAAAACXBIWXMAAAsTAAALEwEAmpwYAAADpmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjA8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjE8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPlBpeGVsbWF0b3IgMy45Ljg8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMjEtMDktMjJUMTU6MDk6MDE8L3htcDpNb2RpZnlEYXRlPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4K/LzAdAAAAAxJREFUCB1j+P//PwAF/gL+n8otEwAAAABJRU5ErkJggg==" + @bad_png "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII%3D" + + @path "priv/test/static/public_uploads/content/posts/preview_images/preview-image-test/original.png" + + describe "handle_preview_image_upload/2" do + test "stores an image given as a data uri" do + File.rm(@path) + + changeset = + %Post{name: "preview-image-test", status: :draft} + |> Post.changeset() + + assert handle_preview_image_upload(changeset, %{"social_media_preview_image" => @png}).valid? + assert File.exists?(@path) + end + + test "adds a validation error if the image is invalid" do + File.rm(@path) + + changeset = + %Post{name: "bad-image-test", status: :draft} + |> Post.changeset() + + refute handle_preview_image_upload(changeset, %{"social_media_preview_image" => @bad_png}).valid? + refute File.exists?(@path) + end + end +end diff --git a/apps/content/test/content_web/uploaders/social_media_preview_test.exs b/apps/content/test/content_web/uploaders/social_media_preview_test.exs new file mode 100644 index 00000000..27558ced --- /dev/null +++ b/apps/content/test/content_web/uploaders/social_media_preview_test.exs @@ -0,0 +1,17 @@ +defmodule Legendary.ContentWeb.Uploaders.SocialMediaPreviewTest do + use Legendary.Content.DataCase + + import Legendary.ContentWeb.Uploaders.SocialMediaPreview + + describe "filename/2" do + test "" do + assert filename(:original, {%{file_name: "original.png"}, nil}) =~ "original" + end + end + + describe "storage_dir/2" do + test "" do + assert storage_dir(nil, {nil, %{name: "test-slug"}}) =~ "public_uploads/content/posts/preview_images/test-slug" + end + end +end diff --git a/apps/core/lib/core_web/base64_uploads.ex b/apps/core/lib/core_web/base64_uploads.ex new file mode 100644 index 00000000..3b28c4b9 --- /dev/null +++ b/apps/core/lib/core_web/base64_uploads.ex @@ -0,0 +1,48 @@ +defmodule Legendary.CoreWeb.Base64Uploads do + @moduledoc """ + Utilities for converting data uris and base64 strings to Plug.Upload structs + so they can be processed in the same way as files submitted by multipart forms. + """ + def data_uri_to_upload(str) do + parse_result = + str + |> URI.parse() + |> URL.Data.parse() + + case parse_result do + %{data: {:error, _}} -> + :error + %{data: data, mediatype: content_type} -> + binary_to_upload(data, content_type) + end + end + + def base64_to_upload(str, content_type) do + case Base.decode64(str) do + {:ok, data} -> binary_to_upload(data, content_type) + _ -> :error + end + end + + def binary_to_upload(binary, content_type) do + file_extension = file_extension_for_content_type(content_type) + + with {:ok, path} <- Plug.Upload.random_file("upload"), + {:ok, file} <- File.open(path, [:write, :binary]), + :ok <- IO.binwrite(file, binary), + :ok <- File.close(file) do + %Plug.Upload{ + path: path, + content_type: content_type, + filename: "#{Path.basename(path)}#{file_extension}" + } + end + end + + defp file_extension_for_content_type(content_type) do + case MIME.extensions(content_type) do + [] -> "" + [ext|_] -> ".#{ext}" + end + end +end diff --git a/apps/core/lib/core_web/views/helpers.ex b/apps/core/lib/core_web/views/helpers.ex index 54191d60..d1bd1dc5 100644 --- a/apps/core/lib/core_web/views/helpers.ex +++ b/apps/core/lib/core_web/views/helpers.ex @@ -73,7 +73,7 @@ defmodule Legendary.CoreWeb.Helpers do """ end - defp do_styled_input_tag(type, input_helper, f, field, nil, opts, classes, error_classes) when type in [:date_select, :time_select, :datetime_select] do + defp do_styled_input_tag(type, _input_helper, f, field, nil, opts, classes, error_classes) when type in [:date_select, :time_select, :datetime_select] do default_child_opts = [ month: [ class: "appearance-none border-b-2 border-dashed", diff --git a/apps/core/mix.exs b/apps/core/mix.exs index 4620a44a..da9fe791 100644 --- a/apps/core/mix.exs +++ b/apps/core/mix.exs @@ -11,7 +11,7 @@ defmodule Legendary.Core.MixProject do config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: "~> 1.7", + elixir: "~> 1.10", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, @@ -142,7 +142,10 @@ defmodule Legendary.Core.MixProject do {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, {:ex_cldr, "~> 2.23.0"}, {:ex_doc, "~> 0.24", only: :dev, runtime: false}, + {:ex_url, "~> 1.3.1"}, {:excoveralls, "~> 0.10", only: [:dev, :test]}, + {:ex_aws, "~> 2.1.2"}, + {:ex_aws_s3, "~> 2.0"}, {:fun_with_flags, "~> 1.6.0"}, {:fun_with_flags_ui, "~> 0.7.2"}, {:phoenix, "~> 1.6.0"}, @@ -157,12 +160,14 @@ defmodule Legendary.Core.MixProject do {:phoenix_live_dashboard, "~> 0.5.0"}, {:phoenix_pubsub, "~> 2.0"}, {:pow, "~> 1.0.25"}, + {:sweet_xml, "~> 0.6"}, {:telemetry_metrics, "~> 0.4"}, {:telemetry_poller, "~> 1.0"}, {:gettext, "~> 0.11"}, {:jason, "~> 1.0"}, {:libcluster, "~> 3.3"}, {:plug_cowboy, "~> 2.0"}, + {:waffle, "~> 1.1"}, ] end diff --git a/apps/core/test/core_web/base64_uploads_test.exs b/apps/core/test/core_web/base64_uploads_test.exs new file mode 100644 index 00000000..8166a82a --- /dev/null +++ b/apps/core/test/core_web/base64_uploads_test.exs @@ -0,0 +1,38 @@ +defmodule Legendary.CoreWeb.Base64UploadsTest do + use Legendary.Core.DataCase + + import Legendary.CoreWeb.Base64Uploads + + @base64 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAMJlWElmTU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAARAAAAcgEyAAIAAAAUAAAAhIdpAAQAAAABAAAAmAAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciAzLjkuOAAAMjAyMTowOToyMiAxNTowOTowMQAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAAYjzhKAAAACXBIWXMAAAsTAAALEwEAmpwYAAADpmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjA8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjcyPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjE8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPlBpeGVsbWF0b3IgMy45Ljg8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMjEtMDktMjJUMTU6MDk6MDE8L3htcDpNb2RpZnlEYXRlPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4K/LzAdAAAAAxJREFUCB1j+P//PwAF/gL+n8otEwAAAABJRU5ErkJggg==" + @bad_base64 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII%3D" + + describe "data_uri_to_upload/1" do + @png "data:image/png;base64,#{@base64}" + @bad_png "data:image/png;base64,#{@bad_base64}" + + test "with a valid data URI" do + assert %Plug.Upload{} = data_uri_to_upload(@png) + end + + test "with an invalid data URI" do + assert :error = data_uri_to_upload(@bad_png) + end + end + + describe "base64_to_upload/2" do + test "makes an upload from a base64 string" do + assert %Plug.Upload{} = base64_to_upload(@base64, "image/png") + end + + test "returns an error for invalid base64" do + assert :error = base64_to_upload(@bad_base64, "image/png") + end + end + + describe "binary_to_upload/2" do + test "with a binary" do + binary = @base64 |> Base.decode64!() + assert %Plug.Upload{} = binary_to_upload(binary, "image/png") + end + end +end diff --git a/apps/object_storage/.formatter.exs b/apps/object_storage/.formatter.exs new file mode 100644 index 00000000..db861480 --- /dev/null +++ b/apps/object_storage/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:ecto], + inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], + subdirectories: ["priv/*/migrations"] +] diff --git a/apps/object_storage/.gitignore b/apps/object_storage/.gitignore new file mode 100644 index 00000000..77e5621f --- /dev/null +++ b/apps/object_storage/.gitignore @@ -0,0 +1,33 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +object_storage-*.tar + +# Ignore assets that are produced by build tools. +/priv/static/assets/ + +# Ignore digested assets cache. +/priv/static/cache_manifest.json + +# In case you use Node.js/npm, you want to ignore these. +npm-debug.log +/assets/node_modules/ diff --git a/apps/object_storage/README.md b/apps/object_storage/README.md new file mode 100644 index 00000000..d8466ec4 --- /dev/null +++ b/apps/object_storage/README.md @@ -0,0 +1,3 @@ +# Legendary.ObjectStorage + +**TODO: Add description** diff --git a/apps/object_storage/cypress.json b/apps/object_storage/cypress.json new file mode 100644 index 00000000..3eb2033a --- /dev/null +++ b/apps/object_storage/cypress.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "http://localhost:4002" +} diff --git a/apps/object_storage/cypress/fixtures/example.json b/apps/object_storage/cypress/fixtures/example.json new file mode 100644 index 00000000..02e42543 --- /dev/null +++ b/apps/object_storage/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/apps/object_storage/cypress/integration/example_spec.js b/apps/object_storage/cypress/integration/example_spec.js new file mode 100644 index 00000000..adb8e603 --- /dev/null +++ b/apps/object_storage/cypress/integration/example_spec.js @@ -0,0 +1,5 @@ +describe('My First Test', () => { + it('Does not do much!', () => { + expect(true).to.equal(true) + }) +}) diff --git a/apps/object_storage/cypress/plugins/index.js b/apps/object_storage/cypress/plugins/index.js new file mode 100644 index 00000000..59b2bab6 --- /dev/null +++ b/apps/object_storage/cypress/plugins/index.js @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/apps/object_storage/cypress/support/commands.js b/apps/object_storage/cypress/support/commands.js new file mode 100644 index 00000000..cc592786 --- /dev/null +++ b/apps/object_storage/cypress/support/commands.js @@ -0,0 +1,36 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +Cypress.Commands.add("setupDB", (app, seedSet) => { + cy.request('POST', '/end-to-end/db/setup', { + app, + seed_set: seedSet, + }).as('setupDB') +}) + +Cypress.Commands.add("teardownDB", () => { + cy.request('POST', '/end-to-end/db/teardown').as('teardownDB') +}) diff --git a/apps/object_storage/cypress/support/index.js b/apps/object_storage/cypress/support/index.js new file mode 100644 index 00000000..7b40d0c6 --- /dev/null +++ b/apps/object_storage/cypress/support/index.js @@ -0,0 +1,29 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +before(() => { + // Make sure we don't have a rogue DB connection checked out + cy.teardownDB() +}) + +afterEach(() => { + cy.teardownDB() +}) diff --git a/apps/object_storage/file.dmg b/apps/object_storage/file.dmg new file mode 100644 index 00000000..e69de29b diff --git a/apps/object_storage/lib/object_storage.ex b/apps/object_storage/lib/object_storage.ex new file mode 100644 index 00000000..1ae4a543 --- /dev/null +++ b/apps/object_storage/lib/object_storage.ex @@ -0,0 +1,11 @@ +defmodule Legendary.ObjectStorage do + @moduledoc """ + Legendary.ObjectStorage keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ + + def bucket_name, do: Application.get_env(:object_storage, :bucket_name) +end diff --git a/apps/object_storage/lib/object_storage/application.ex b/apps/object_storage/lib/object_storage/application.ex new file mode 100644 index 00000000..71f9e50d --- /dev/null +++ b/apps/object_storage/lib/object_storage/application.ex @@ -0,0 +1,35 @@ +defmodule Legendary.ObjectStorage.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + alias Legendary.ObjectStorageWeb.Endpoint + + @impl true + def start(_type, _args) do + children = [ + # Start the Telemetry supervisor + Legendary.ObjectStorageWeb.Telemetry, + # Start the Ecto repository + Legendary.ObjectStorage.Repo, + # Start the Endpoint (http/https) + Legendary.ObjectStorageWeb.Endpoint, + # Start the PubSub system + {Phoenix.PubSub, name: Legendary.ObjectStorage.PubSub} + # Start a worker by calling Legendary.ObjectStorage.Worker.start_link(arg) + # {ObjectStorage.Worker, arg} + ] + + Supervisor.start_link(children, strategy: :one_for_one, name: Legendary.ObjectStorage.Supervisor) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + @impl true + def config_change(changed, _new, removed) do + Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/apps/object_storage/lib/object_storage/object.ex b/apps/object_storage/lib/object_storage/object.ex new file mode 100644 index 00000000..c20aa96c --- /dev/null +++ b/apps/object_storage/lib/object_storage/object.ex @@ -0,0 +1,35 @@ +defmodule Legendary.ObjectStorage.Object do + @moduledoc """ + One object/file in the object storage app. + """ + use Ecto.Schema + import Ecto.Changeset + + @acl_values [:private, :public_read] + + schema "storage_objects" do + field :acl, Ecto.Enum, values: @acl_values + field :body, :binary + field :path, :string + + timestamps() + end + + @doc false + def changeset(object, attrs \\ %{}) do + object + |> cast(attrs, [:path, :body, :acl]) + |> validate_required([:path, :acl]) + |> validate_body_or_upload(attrs) + |> validate_inclusion(:acl, @acl_values, message: "is not supported. Valid values are #{@acl_values |> Enum.map(&Atom.to_string/1) |> Enum.join(",")}.") + end + + defp validate_body_or_upload(changeset, attrs) do + case attrs do + %{uploads: "1"} -> + changeset + _ -> + validate_required(changeset, :body) + end + end +end diff --git a/apps/object_storage/lib/object_storage/object_chunk.ex b/apps/object_storage/lib/object_storage/object_chunk.ex new file mode 100644 index 00000000..a208587c --- /dev/null +++ b/apps/object_storage/lib/object_storage/object_chunk.ex @@ -0,0 +1,28 @@ +defmodule Legendary.ObjectStorage.ObjectChunk do + @moduledoc """ + One chunk of a chunked upload. + """ + use Ecto.Schema + import Ecto.Changeset + + schema "storage_object_chunks" do + field :body, :binary + field :part_number, :integer + field :path, :string + + timestamps() + end + + @doc false + def changeset(object_chunk, attrs) do + object_chunk + |> cast(attrs, [:path, :body, :part_number]) + |> validate_required([:path, :body, :part_number]) + end + + def etag(chunk) do + key = "#{chunk.path}:#{chunk.part_number}:#{chunk.inserted_at}" + + Base.encode16(:crypto.hash(:md5 , key)) + end +end diff --git a/apps/object_storage/lib/object_storage/objects.ex b/apps/object_storage/lib/object_storage/objects.ex new file mode 100644 index 00000000..959804d0 --- /dev/null +++ b/apps/object_storage/lib/object_storage/objects.ex @@ -0,0 +1,145 @@ +defmodule Legendary.ObjectStorage.Objects do + @moduledoc """ + The Objects context. + """ + + import Ecto.Query, warn: false + + alias Legendary.ObjectStorage.{Object, ObjectChunk} + alias Legendary.ObjectStorage.Repo + + @doc """ + Gets a single object. + + Raises if the Object does not exist. + + ## Examples + + iex> get_object!(123) + %Object{} + + """ + @spec get_object(binary) :: {:ok, Object.t()} | {:error, :not_found} + def get_object(path) do + from( + obj in Object, + where: obj.path == ^path + ) + |> Repo.one() + |> case do + nil -> {:error, :not_found} + %Object{} = object -> {:ok, object} + end + end + + @spec get_or_initialize_object(binary) :: Object.t() + def get_or_initialize_object(path) do + case get_object(path) do + {:ok, object} -> + object + {:error, :not_found} -> + %Object{} + end + end + + @doc """ + Updates a object. + + ## Examples + + iex> update_object(object, %{field: new_value}) + {:ok, %Object{}} + + iex> update_object(object, %{field: bad_value}) + {:error, ...} + + """ + @spec update_object(Object.t(), Map.t()) :: {:ok, Object.t()} | {:error, Ecto.Changeset.t()} + def update_object(%Object{} = object, attrs) do + object + |> Object.changeset(attrs) + |> Repo.insert_or_update() + end + + def put_chunk(%{path: path}, %{part_number: part_number, body: body}) do + %ObjectChunk{} + |> ObjectChunk.changeset(%{ + path: path, + part_number: part_number, + body: body + }) + |> Repo.insert(conflict_target: [:path, :part_number], on_conflict: {:replace, [:body]}) + end + + def finalize_chunked_upload(%Object{path: path}, request_etags) do + chunk_query = + from(chunk in ObjectChunk, where: chunk.path == ^path) + + part_number_range = + chunk_query + |> select([c], [ + min_chunk: fragment("min(part_number)"), + max_chunk: fragment("max(part_number)"), + chunk_count: fragment("count(part_number)") + ]) + |> Repo.one() + |> Enum.into(%{}) + + Ecto.Multi.new() + |> Ecto.Multi.run(:check_chunks, fn _repo, _ -> + case part_number_range do + %{min_chunk: 1, max_chunk: max_chunk, chunk_count: max_chunk} -> + {:ok, part_number_range} + _ -> + {:error, "Missing chunks for chunked upload. Aborting."} + end + end) + |> Ecto.Multi.run(:check_etags, fn _repo, _ -> + db_etags = + chunk_query + |> Repo.all() + |> Enum.map(&ObjectChunk.etag/1) + |> MapSet.new() + + if db_etags == MapSet.new(request_etags) do + {:ok, request_etags} + else + {:error, "ETags in request do not match parts in database."} + end + end) + |> Ecto.Multi.update_all(:update_object_body, fn %{} -> + from( + object in Object, + where: object.path == ^path, + join: new_body in fragment(""" + SELECT string_agg(body, '') as body + FROM ( + SELECT body + FROM storage_object_chunks + WHERE path = ? + ORDER BY part_number ASC + ) as body_pieces + """, ^path), + update: [set: [body: new_body.body]] + ) + end, []) + |> Ecto.Multi.delete_all(:remove_chunks, chunk_query) + |> Repo.transaction() + end + + @doc """ + Deletes a Object. + + ## Examples + + iex> delete_object(object) + {:ok, %Object{}} + + iex> delete_object(object) + {:error, ...} + + """ + def delete_object(%Object{} = object) do + Repo.delete(object) + end +end diff --git a/apps/object_storage/lib/object_storage/repo.ex b/apps/object_storage/lib/object_storage/repo.ex new file mode 100644 index 00000000..f5ba5cc9 --- /dev/null +++ b/apps/object_storage/lib/object_storage/repo.ex @@ -0,0 +1,5 @@ +defmodule Legendary.ObjectStorage.Repo do + use Ecto.Repo, + otp_app: :object_storage, + adapter: Ecto.Adapters.Postgres +end diff --git a/apps/object_storage/lib/object_storage_web.ex b/apps/object_storage/lib/object_storage_web.ex new file mode 100644 index 00000000..26e60669 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web.ex @@ -0,0 +1,105 @@ +defmodule Legendary.ObjectStorageWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, views, channels and so on. + + This can be used in your application as: + + use Legendary.ObjectStorageWeb, :controller + use Legendary.ObjectStorageWeb, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define any helper function in modules + and import those modules here. + """ + + def controller do + quote do + use Phoenix.Controller, namespace: Legendary.ObjectStorageWeb + + import Plug.Conn + import Legendary.ObjectStorageWeb.Gettext + import Legendary.ObjectStorageWeb.Helpers + alias Legendary.ObjectStorageWeb.Router.Helpers, as: Routes + + plug :parse_body + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/object_storage_web/templates", + namespace: Legendary.ObjectStorageWeb + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] + + # Include shared imports and aliases for views + unquote(view_helpers()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {Legendary.ObjectStorageWeb.LayoutView, "live.html"} + + unquote(view_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(view_helpers()) + end + end + + def router do + quote do + use Phoenix.Router + + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + import Legendary.ObjectStorageWeb.Gettext + end + end + + defp view_helpers do + quote do + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + + # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) + import Phoenix.LiveView.Helpers + + # Import basic rendering functionality (render, render_layout, etc) + import Phoenix.View + + import Legendary.ObjectStorageWeb.ErrorHelpers + import Legendary.ObjectStorageWeb.Gettext + alias Legendary.ObjectStorageWeb.Router.Helpers, as: Routes + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/apps/object_storage/lib/object_storage_web/controllers/chunked_upload_controller.ex b/apps/object_storage/lib/object_storage_web/controllers/chunked_upload_controller.ex new file mode 100644 index 00000000..9025c733 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/controllers/chunked_upload_controller.ex @@ -0,0 +1,81 @@ +defmodule Legendary.ObjectStorageWeb.ChunkedUploadController do + use Legendary.ObjectStorageWeb, :controller + + alias Ecto.Changeset + alias Legendary.ObjectStorage.Objects + + plug :put_view, Legendary.ObjectStorageWeb.UploadView + plug Legendary.ObjectStorageWeb.CheckSignatures when action not in [:show] + + def chunked_upload(conn, %{"path" => path_parts, "uploads" => "1"}), do: start(conn, path_parts) + def chunked_upload(conn, %{"path" => path_parts, "uploadId" => id}) when is_binary(id), do: finalize(conn, path_parts, id) + + defp start(conn, path_parts) do + path = Enum.join(path_parts, "/") + + attrs = %{ + path: path, + acl: get_first_request_header(conn, "x-amz-acl", "private"), + uploads: "1" + } + + object = Objects.get_or_initialize_object(path) + + case Objects.update_object(object, attrs) do + {:ok, updated_object} -> + render(conn, "initiate_multipart_upload.xml", %{object: updated_object}) + {:error, %Changeset{} = changeset} -> + conn + |> put_status(:bad_request) + |> render("error.xml", changeset: changeset) + end + end + + defp finalize(conn, path_parts, _id) do + path = Enum.join(path_parts, "/") + + with {:ok, object} <- Objects.get_object(path), + {:ok, etags} <- extract_etags(conn), + {:ok, _} <- Objects.finalize_chunked_upload(object, etags) do + send_resp(conn, :ok, "") + else + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> render("not_found.xml") + {:error, message} -> + conn + |> put_status(:bad_request) + |> render("error.xml", message: message, code: "InvalidPart", path: path) + {:error, _, message, _} -> + conn + |> put_status(:bad_request) + |> render("error.xml", message: message, code: "InvalidPart", path: path) + end + end + + defp extract_etags(%{assigns: %{body: body}}) do + xpath = + %SweetXpath{ + path: '//Part/ETag/text()', + is_value: true, + cast_to: false, + is_list: true, + is_keyword: false + } + + try do + {:ok, + body + |> SweetXml.parse(quiet: true) + |> SweetXml.xpath(xpath) + |> Enum.map(&to_string/1) + } + catch + :exit, _ -> + {:error, "Missing etags for chunked upload."} + end + end + + defp extract_etags(_), do: {:error, "Missing etags for chunked upload."} +end diff --git a/apps/object_storage/lib/object_storage_web/controllers/fallback_controller.ex b/apps/object_storage/lib/object_storage_web/controllers/fallback_controller.ex new file mode 100644 index 00000000..132b54c7 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/controllers/fallback_controller.ex @@ -0,0 +1,16 @@ +defmodule Legendary.ObjectStorageWeb.FallbackController do + @moduledoc """ + Translates controller action results into valid `Plug.Conn` responses. + + See `Phoenix.Controller.action_fallback/1` for more details. + """ + use Legendary.ObjectStorageWeb, :controller + + # This clause is an example of how to handle resources that cannot be found. + def call(conn, {:error, :not_found}) do + conn + |> put_status(:not_found) + |> put_view(ObjectStorageWeb.ErrorView) + |> render(:"404") + end +end diff --git a/apps/object_storage/lib/object_storage_web/controllers/upload_controller.ex b/apps/object_storage/lib/object_storage_web/controllers/upload_controller.ex new file mode 100644 index 00000000..f6f394d6 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/controllers/upload_controller.ex @@ -0,0 +1,106 @@ +defmodule Legendary.ObjectStorageWeb.UploadController do + use Legendary.ObjectStorageWeb, :controller + + alias Ecto.Changeset + alias Legendary.ObjectStorage.Objects + alias Legendary.ObjectStorage.{Object, ObjectChunk} + alias Legendary.ObjectStorageWeb.CheckSignatures + + action_fallback ObjectStorageWeb.FallbackController + + plug CheckSignatures when action not in [:show] + + def show(conn, %{"path" => path_parts}) do + case Objects.get_object(Enum.join(path_parts, "/")) do + {:ok, %{acl: :public_read} = object} -> + conn + |> put_resp_content_type(MIME.from_path(object.path) , "binary") + |> send_resp(200, object.body) + {:ok, %{acl: :private} = object} -> + conn_checked = CheckSignatures.call(conn) + + if conn_checked.halted do + conn + |> put_status(:not_found) + |> render("not_found.xml") + else + conn + |> put_resp_content_type(MIME.from_path(object.path) , "binary") + |> send_resp(200, object.body) + end + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> render("not_found.xml") + end + end + + def put_object( + conn, + %{"path" => path_parts, "uploadId" => _id, "partNumber" => part_number} + ) when is_binary(part_number), do: put_chunk(conn, path_parts, part_number) + + def put_object(conn, %{"path" => path_parts}), do: do_put_object(conn, path_parts) + + def delete_object(conn, %{"path" => path_parts}) do + with {:ok, object} <- Objects.get_object(Enum.join(path_parts, "/")), + {:ok, %Object{}} <- Objects.delete_object(object) + do + send_resp(conn, :no_content, "") + else + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> render("not_found.xml") + end + end + + defp put_chunk(conn, path_parts, part_number) do + path = Enum.join(path_parts, "/") + + attrs = %{ + body: conn.assigns.body, + part_number: part_number + } + + with {:ok, object} <- Objects.get_object(path), + {:ok, chunk} <- Objects.put_chunk(object, attrs) + do + conn + |> put_resp_header("etag", ObjectChunk.etag(chunk)) + |> send_resp(:ok, "") + else + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> render("not_found.xml") + {:error, %Changeset{} = changeset} -> + conn + |> put_status(:bad_request) + |> render("error.xml", changeset: changeset) + end + end + + defp do_put_object(conn, path_parts) do + path = Enum.join(path_parts, "/") + + attrs = %{ + path: path, + body: conn.assigns.body, + acl: get_first_request_header(conn, "x-amz-acl", "private"), + } + + object = Objects.get_or_initialize_object(path) + + case Objects.update_object(object, attrs) do + {:ok, _} -> + conn + |> put_resp_content_type("application/text") + |> send_resp(:ok, "") + {:error, %Changeset{} = changeset} -> + conn + |> put_status(:bad_request) + |> render("error.xml", changeset: changeset) + end + end +end diff --git a/apps/object_storage/lib/object_storage_web/endpoint.ex b/apps/object_storage/lib/object_storage_web/endpoint.ex new file mode 100644 index 00000000..ccf7e28a --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/endpoint.ex @@ -0,0 +1,40 @@ +defmodule Legendary.ObjectStorageWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :object_storage + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + @session_options [ + store: :cookie, + key: "_object_storage_web_key", + signing_salt: "bsLgw5QW" + ] + + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :object_storage + end + + plug Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger", + cookie_key: "request_logger" + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug Legendary.ObjectStorageWeb.Router +end diff --git a/apps/object_storage/lib/object_storage_web/gettext.ex b/apps/object_storage/lib/object_storage_web/gettext.ex new file mode 100644 index 00000000..440e429b --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/gettext.ex @@ -0,0 +1,24 @@ +defmodule Legendary.ObjectStorageWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), + your module gains a set of macros for translations, for example: + + import Legendary.ObjectStorageWeb.Gettext + + # Simple translation + gettext("Here is the string to translate") + + # Plural translation + ngettext("Here is the string to translate", + "Here are the strings to translate", + 3) + + # Domain-based translation + dgettext("errors", "Here is the error message to translate") + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext, otp_app: :object_storage +end diff --git a/apps/object_storage/lib/object_storage_web/helpers.ex b/apps/object_storage/lib/object_storage_web/helpers.ex new file mode 100644 index 00000000..89620774 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/helpers.ex @@ -0,0 +1,34 @@ +defmodule Legendary.ObjectStorageWeb.Helpers do + @moduledoc """ + Utility functions which are used throughout ObjectStorageWeb. + """ + + alias Plug.Conn + + def get_first_request_header(conn, key, default \\ nil) do + case Conn.get_req_header(conn, key) do + [] -> default + [hd|_] -> hd + end + end + + def parse_body(conn, _opts) do + {:ok, body, conn} = + conn + |> Conn.read_body() + + Conn.assign(conn, :body, body) + end + + def amz_date_parse(date_string) do + format = ~r/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z/ + [_|parts] = Regex.run(format, date_string) + + [year, month, day, hour, minute, second] = + parts + |> Enum.map(&Integer.parse/1) + |> Enum.map(fn {int, _} -> int end) + + {{year, month, day}, {hour, minute, second}} + end +end diff --git a/apps/object_storage/lib/object_storage_web/plugs/check_signatures.ex b/apps/object_storage/lib/object_storage_web/plugs/check_signatures.ex new file mode 100644 index 00000000..0d4dc083 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/plugs/check_signatures.ex @@ -0,0 +1,70 @@ +defmodule Legendary.ObjectStorageWeb.CheckSignatures do + @moduledoc """ + A plug for checking authorization signatures either in the headers or query params. + """ + @behaviour Plug + + import Plug.Conn + import Legendary.ObjectStorageWeb.Helpers, only: [get_first_request_header: 2, amz_date_parse: 1] + + alias Legendary.ObjectStorageWeb.CheckSignatures.SignatureGenerator + + def init(opts) do + opts + end + + def call(conn, _opts \\ []) do + with true <- fresh_request(conn), + {:ok, correct_signature} <- signature_generator().correct_signature_for_conn(conn), + {:ok, actual_signature} <- actual_signature_for_conn(conn), + true <- Plug.Crypto.secure_compare(correct_signature, actual_signature) + do + conn + else + _ -> + conn + |> send_resp(:forbidden, "Forbidden") + |> halt() + end + end + + def actual_signature_for_conn(%{query_params: %{"X-Amz-Signature" => actual_signature}}) do + {:ok, actual_signature} + end + + def actual_signature_for_conn(conn) do + %{"Signature" => actual_signature} = + conn + |> get_first_request_header("authorization") + |> signature_generator().parse_authorization_header() + + {:ok, actual_signature} + end + + defp signature_generator() do + Application.get_env(:object_storage, :signature_generator, SignatureGenerator) + end + + @one_week 60 * 60 * 24 * 7 + defp fresh_request(%{ + query_params: %{ + "X-Amz-Expires" => expires_in, + "X-Amz-Date" => request_timestamp, + } + }) when is_integer(expires_in) do + request_epoch = + request_timestamp + |> amz_date_parse() + |> :calendar.datetime_to_gregorian_seconds() + + now_epoch = + :calendar.universal_time() + |> :calendar.datetime_to_gregorian_seconds() + + request_age = now_epoch - request_epoch + + request_age < expires_in && expires_in < @one_week + end + + defp fresh_request(_), do: true +end diff --git a/apps/object_storage/lib/object_storage_web/plugs/check_signatures/signature_generator.ex b/apps/object_storage/lib/object_storage_web/plugs/check_signatures/signature_generator.ex new file mode 100644 index 00000000..2c515cb7 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/plugs/check_signatures/signature_generator.ex @@ -0,0 +1,103 @@ +defmodule Legendary.ObjectStorageWeb.CheckSignatures.SignatureGenerator do + @moduledoc """ + Can generate a signature based on an incoming request so that it can be verified + against the signature header or parameter submitted. + """ + + import Legendary.ObjectStorageWeb.Helpers, only: [get_first_request_header: 2, amz_date_parse: 1] + + alias ExAws.{ + Auth, + Auth.Credentials, + Auth.Signatures, + Request.Url + } + + alias ExAws.Auth.Utils, as: AuthUtils + alias ExAws.S3.Utils, as: S3Utils + + alias Plug.Conn + + @callback correct_signature_for_conn(Conn.t()) :: {:ok, String.t()} + @callback parse_authorization_header(String.t()) :: Map.t() + + @signature_in_query_pattern ~r/(&X-Amz-Signature=[0-9a-fA-F]+)|(X-Amz-Signature=[0-9a-fA-F]+&)/ + def correct_signature_for_conn(conn) do + config = ExAws.Config.new(:s3) + url = url_to_sign(conn, config) + sanitized_query_string = Regex.replace(@signature_in_query_pattern, conn.query_string, "") + + {:ok, signature( + conn.method |> String.downcase() |> String.to_atom(), + url, + sanitized_query_string, + filtered_headers(conn), + body_for_request(conn), + conn |> request_datetime() |> amz_date_parse(), + config + )} + end + + def parse_authorization_header(header) do + ["AWS4-HMAC-SHA256", params] = String.split(header, " ") + + params + |> String.split(",") + |> Enum.map(& String.split(&1, "=")) + |> Enum.map(fn [k, v] -> {k, v} end) + |> Enum.into(%{}) + end + + defp url_to_sign(%{params: %{"path" => path_parts}}, config) do + object = + path_parts + |> Enum.join("/") + |> S3Utils.ensure_slash() + bucket = Application.get_env(:object_storage, :bucket_name) + port = S3Utils.sanitized_port_component(config) + "#{config[:scheme]}#{config[:host]}#{port}/#{bucket}#{object}" + end + + defp request_datetime(%{params: %{"X-Amz-Date" => datetime}}), do: datetime + defp request_datetime(conn), do: get_first_request_header(conn, "x-amz-date") + + defp filtered_headers(conn) do + signed_header_keys = + case conn.params do + %{"X-Amz-SignedHeaders" => signed_header_string} -> + signed_header_string + _ -> + conn + |> get_first_request_header("authorization") + |> parse_authorization_header() + |> Map.get("SignedHeaders") + end + |> String.split(";") + + Enum.filter(conn.req_headers, fn {k, _v} -> k in signed_header_keys end) + end + + # Presigned URL, so do not include body (unknown when presigning) to sig calc + defp body_for_request(%{params: %{"X-Amz-Signature" => _}}), do: nil + # Otherwise, include body + defp body_for_request(%{assigns: %{body: body}}), do: body + + defp signature(http_method, url, query, headers, body, datetime, config) do + path = url |> Url.get_path(:s3) |> Url.uri_encode() + request = Auth.build_canonical_request(http_method, path, query, headers, body) + string_to_sign = string_to_sign(request, :s3, datetime, config) + Signatures.generate_signature_v4("s3", config, datetime, string_to_sign) + end + + defp string_to_sign(request, service, datetime, config) do + request = AuthUtils.hash_sha256(request) + + """ + AWS4-HMAC-SHA256 + #{AuthUtils.amz_date(datetime)} + #{Credentials.generate_credential_scope_v4(service, config, datetime)} + #{request} + """ + |> String.trim_trailing() + end +end diff --git a/apps/object_storage/lib/object_storage_web/router.ex b/apps/object_storage/lib/object_storage_web/router.ex new file mode 100644 index 00000000..1d86ba0b --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/router.ex @@ -0,0 +1,39 @@ +defmodule Legendary.ObjectStorageWeb.Router do + use Legendary.ObjectStorageWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, {Legendary.ObjectStorageWeb.LayoutView, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + pipeline :api do + plug :accepts, ["json"] + end + + use Legendary.ObjectStorageWeb.Routes + + # Other scopes may use custom stacks. + # scope "/api", Legendary.ObjectStorageWeb do + # pipe_through :api + # end + + # Enables LiveDashboard only for development + # + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). + if Mix.env() in [:dev, :test] do + import Phoenix.LiveDashboard.Router + + scope "/" do + pipe_through :browser + live_dashboard "/dashboard", metrics: Legendary.ObjectStorageWeb.Telemetry + end + end +end diff --git a/apps/object_storage/lib/object_storage_web/routes.ex b/apps/object_storage/lib/object_storage_web/routes.ex new file mode 100644 index 00000000..2a30bbe5 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/routes.ex @@ -0,0 +1,21 @@ +defmodule Legendary.ObjectStorageWeb.Routes do + @moduledoc """ + Routes for the object storage engine. + """ + + import Legendary.ObjectStorage, only: [bucket_name: 0] + + defmacro __using__(_opts \\ []) do + quote do + scope "/", Legendary.ObjectStorageWeb do + pipe_through :api + + get "/#{bucket_name()}/*path", UploadController, :show + put "/#{bucket_name()}/*path", UploadController, :put_object + delete "/#{bucket_name()}/*path", UploadController, :delete_object + + post "/#{bucket_name()}/*path", ChunkedUploadController, :chunked_upload + end + end + end +end diff --git a/apps/object_storage/lib/object_storage_web/telemetry.ex b/apps/object_storage/lib/object_storage_web/telemetry.ex new file mode 100644 index 00000000..fd859151 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/telemetry.ex @@ -0,0 +1,74 @@ +defmodule Legendary.ObjectStorageWeb.Telemetry do + @moduledoc """ + Metric definitions for the object storage app. + """ + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + + # Database Metrics + summary("object_storage_web.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("object_storage_web.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("object_storage_web.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("object_storage_web.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("object_storage_web.repo.query.idle_time", + unit: {:native, :millisecond}, + description: + "The time the connection spent waiting before being checked out for the query" + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {ObjectStorageWeb, :count_users, []} + ] + end +end diff --git a/apps/object_storage/lib/object_storage_web/templates/layout/app.html.heex b/apps/object_storage/lib/object_storage_web/templates/layout/app.html.heex new file mode 100644 index 00000000..169aed95 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/templates/layout/app.html.heex @@ -0,0 +1,5 @@ +
+ + + <%= @inner_content %> +
diff --git a/apps/object_storage/lib/object_storage_web/templates/layout/live.html.heex b/apps/object_storage/lib/object_storage_web/templates/layout/live.html.heex new file mode 100644 index 00000000..a29d6044 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/templates/layout/live.html.heex @@ -0,0 +1,11 @@ +
+ + + + + <%= @inner_content %> +
diff --git a/apps/object_storage/lib/object_storage_web/templates/layout/root.html.heex b/apps/object_storage/lib/object_storage_web/templates/layout/root.html.heex new file mode 100644 index 00000000..2ac90d15 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/templates/layout/root.html.heex @@ -0,0 +1,30 @@ + + + + + + + <%= csrf_meta_tag() %> + <%= live_title_tag assigns[:page_title] || "ObjectStorageWeb", suffix: " · Phoenix Framework" %> + + + + +
+
+ + +
+
+ <%= @inner_content %> + + diff --git a/apps/object_storage/lib/object_storage_web/templates/page/index.html.heex b/apps/object_storage/lib/object_storage_web/templates/page/index.html.heex new file mode 100644 index 00000000..f844bd8d --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/templates/page/index.html.heex @@ -0,0 +1,41 @@ +
+

<%= gettext "Welcome to %{name}!", name: "Phoenix" %>

+

Peace of mind from prototype to production

+
+ +
+ + +
diff --git a/apps/object_storage/lib/object_storage_web/views/changeset_view.ex b/apps/object_storage/lib/object_storage_web/views/changeset_view.ex new file mode 100644 index 00000000..9c2434c6 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/views/changeset_view.ex @@ -0,0 +1,19 @@ +defmodule ObjectStorageWeb.ChangesetView do + use Legendary.ObjectStorageWeb, :view + + @doc """ + Traverses and translates changeset errors. + + See `Ecto.Changeset.traverse_errors/2` and + `ObjectStorageWeb.ErrorHelpers.translate_error/1` for more details. + """ + def translate_errors(changeset) do + Ecto.Changeset.traverse_errors(changeset, &translate_error/1) + end + + def render("error.json", %{changeset: changeset}) do + # When encoded, the changeset returns its errors + # as a JSON object. So we just pass it forward. + %{errors: translate_errors(changeset)} + end +end diff --git a/apps/object_storage/lib/object_storage_web/views/error_helpers.ex b/apps/object_storage/lib/object_storage_web/views/error_helpers.ex new file mode 100644 index 00000000..cb52b7d5 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/views/error_helpers.ex @@ -0,0 +1,47 @@ +defmodule Legendary.ObjectStorageWeb.ErrorHelpers do + @moduledoc """ + Conveniences for translating and building error messages. + """ + + use Phoenix.HTML + + @doc """ + Generates tag for inlined form input errors. + """ + def error_tag(form, field) do + Enum.map(Keyword.get_values(form.errors, field), fn error -> + content_tag(:span, translate_error(error), + class: "invalid-feedback", + phx_feedback_for: input_name(form, field) + ) + end) + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate "is invalid" in the "errors" domain + # dgettext("errors", "is invalid") + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # Because the error messages we show in our forms and APIs + # are defined inside Ecto, we need to translate them dynamically. + # This requires us to call the Gettext module passing our gettext + # backend as first argument. + # + # Note we use the "errors" domain, which means translations + # should be written to the errors.po file. The :count option is + # set by Ecto and indicates we should also apply plural rules. + if count = opts[:count] do + Gettext.dngettext(ObjectStorageWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(ObjectStorageWeb.Gettext, "errors", msg, opts) + end + end +end diff --git a/apps/object_storage/lib/object_storage_web/views/error_view.ex b/apps/object_storage/lib/object_storage_web/views/error_view.ex new file mode 100644 index 00000000..f29a4dd0 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/views/error_view.ex @@ -0,0 +1,16 @@ +defmodule Legendary.ObjectStorageWeb.ErrorView do + use Legendary.ObjectStorageWeb, :view + + # If you want to customize a particular status code + # for a certain format, you may uncomment below. + # def render("500.html", _assigns) do + # "Internal Server Error" + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.html" becomes + # "Not Found". + def template_not_found(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/apps/object_storage/lib/object_storage_web/views/layout_view.ex b/apps/object_storage/lib/object_storage_web/views/layout_view.ex new file mode 100644 index 00000000..491d7c5a --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/views/layout_view.ex @@ -0,0 +1,7 @@ +defmodule Legendary.ObjectStorageWeb.LayoutView do + use Legendary.ObjectStorageWeb, :view + + # Phoenix LiveDashboard is available only in development by default, + # so we instruct Elixir to not warn if the dashboard route is missing. + @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} +end diff --git a/apps/object_storage/lib/object_storage_web/views/page_view.ex b/apps/object_storage/lib/object_storage_web/views/page_view.ex new file mode 100644 index 00000000..6f462271 --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/views/page_view.ex @@ -0,0 +1,3 @@ +defmodule Legendary.ObjectStorageWeb.PageView do + use Legendary.ObjectStorageWeb, :view +end diff --git a/apps/object_storage/lib/object_storage_web/views/upload_view.ex b/apps/object_storage/lib/object_storage_web/views/upload_view.ex new file mode 100644 index 00000000..f9ef19df --- /dev/null +++ b/apps/object_storage/lib/object_storage_web/views/upload_view.ex @@ -0,0 +1,62 @@ +defmodule Legendary.ObjectStorageWeb.UploadView do + use Legendary.ObjectStorageWeb, :view + alias Ecto.Changeset + alias Legendary.ObjectStorage + + def render("initiate_multipart_upload.xml", %{object: object}) do + ~E""" + + + <%= ObjectStorage.bucket_name() %> + <%= object.path %> + <%= object.id %> + + """ + |> safe_to_string() + end + + def render("error.xml", assigns) do + errors = + case assigns do + %{message: message} -> message + %{changeset: changeset} -> + changeset.errors + |> Enum.map(fn {key, {message, _}} -> + "#{key}: #{message}" + end) + |> Enum.join(", ") + end + + code = Map.get(assigns, :code, "InvalidArgument") + + path = + case assigns do + %{changeset: changeset} -> + Changeset.get_field(changeset, :path) + %{path: path} -> + path + end + + + ~E""" + + + <%= code %> + <%= errors %> + <%= path %> + DEADBEEF + + """ + |> safe_to_string() + end + + def render("not_found.xml", _assigns) do + ~E""" + + + NoSuchKey + + """ + |> safe_to_string() + end +end diff --git a/apps/object_storage/mix.exs b/apps/object_storage/mix.exs new file mode 100644 index 00000000..fd73ccdc --- /dev/null +++ b/apps/object_storage/mix.exs @@ -0,0 +1,80 @@ +defmodule Legendary.ObjectStorage.MixProject do + use Mix.Project + + @version "4.2.0" + + def project do + [ + app: :object_storage, + version: @version, + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + elixir: "~> 1.10", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps(), + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test], + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {Legendary.ObjectStorage.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + {:core, in_umbrella: true}, + {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, + {:ecto_sql, "~> 3.6"}, + {:ex_aws, "~> 2.0"}, + {:ex_aws_s3, "~> 2.0"}, + {:floki, ">= 0.30.0", only: :test}, + {:gettext, "~> 0.18"}, + {:hackney, "~> 1.9"}, + {:jason, "~> 1.2"}, + {:mox, "~> 1.0", only: :test}, + {:phoenix, "~> 1.6.0"}, + {:phoenix_ecto, "~> 4.4"}, + {:phoenix_html, "~> 3.0"}, + {:phoenix_live_dashboard, "~> 0.5"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 0.16.0"}, + {:phoenix_pubsub, "~> 2.0"}, + {:plug_cowboy, "~> 2.5"}, + {:postgrex, ">= 0.0.0"}, + {:sweet_xml, "~> 0.7.1"}, + {:telemetry_metrics, "~> 0.6"}, + {:telemetry_poller, "~> 1.0"} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + "npm.install": [], + setup: ["deps.get", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] + ] + end +end diff --git a/apps/object_storage/priv/repo/migrations/.formatter.exs b/apps/object_storage/priv/repo/migrations/.formatter.exs new file mode 100644 index 00000000..49f9151e --- /dev/null +++ b/apps/object_storage/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/apps/object_storage/priv/repo/migrations/20210928031038_create_storage_objects.exs b/apps/object_storage/priv/repo/migrations/20210928031038_create_storage_objects.exs new file mode 100644 index 00000000..a7f28c4b --- /dev/null +++ b/apps/object_storage/priv/repo/migrations/20210928031038_create_storage_objects.exs @@ -0,0 +1,15 @@ +defmodule ObjectStorage.Repo.Migrations.CreateStorageObjects do + use Ecto.Migration + + def change do + create table(:storage_objects) do + add :path, :string + add :body, :binary + add :acl, :string + + timestamps() + end + + create unique_index(:storage_objects, :path) + end +end diff --git a/apps/object_storage/priv/repo/migrations/20211005220041_create_storage_object_chunk.exs b/apps/object_storage/priv/repo/migrations/20211005220041_create_storage_object_chunk.exs new file mode 100644 index 00000000..1fb9f082 --- /dev/null +++ b/apps/object_storage/priv/repo/migrations/20211005220041_create_storage_object_chunk.exs @@ -0,0 +1,15 @@ +defmodule ObjectStorage.Repo.Migrations.CreateStorageObjectChunk do + use Ecto.Migration + + def change do + create table(:storage_object_chunks) do + add :path, :string + add :body, :binary + add :part_number, :integer + + timestamps() + end + + create unique_index(:storage_object_chunks, [:path, :part_number]) + end +end diff --git a/apps/object_storage/priv/repo/seeds.exs b/apps/object_storage/priv/repo/seeds.exs new file mode 100644 index 00000000..a9d7a7cb --- /dev/null +++ b/apps/object_storage/priv/repo/seeds.exs @@ -0,0 +1,11 @@ +# Script for populating the database. You can run it as: +# +# mix run priv/repo/seeds.exs +# +# Inside the script, you can read and write to any of your +# repositories directly: +# +# Legendary.ObjectStorage.Repo.insert!(%ObjectStorage.SomeSchema{}) +# +# We recommend using the bang functions (`insert!`, `update!` +# and so on) as they will fail if something goes wrong. diff --git a/apps/object_storage/test.dmg b/apps/object_storage/test.dmg new file mode 100644 index 00000000..e69de29b diff --git a/apps/object_storage/test/object_storage/object_chunk_test.exs b/apps/object_storage/test/object_storage/object_chunk_test.exs new file mode 100644 index 00000000..0a2b4945 --- /dev/null +++ b/apps/object_storage/test/object_storage/object_chunk_test.exs @@ -0,0 +1,33 @@ +defmodule Legendary.ObjectStorage.ObjectChunkTest do + use Legendary.ObjectStorage.DataCase + + import Legendary.ObjectStorage.ObjectChunk + + alias Legendary.ObjectStorage.ObjectChunk + + describe "changeset/2" do + test "requires path, part_number, and body" do + chunk = %ObjectChunk{path: "test.txt", part_number: 1, body: "hello!"} + + assert changeset(chunk, %{}).valid? + refute changeset(chunk, %{path: nil}).valid? + refute changeset(chunk, %{body: nil}).valid? + refute changeset(chunk, %{part_number: nil}).valid? + end + end + + describe "etag/1" do + test "is the same if the path, part, and inserted_at are same" do + chunk = %{path: "hello-world", part_number: 1, inserted_at: DateTime.utc_now()} + assert etag(chunk) == etag(chunk) + end + + test "is different if the path, part, or inserted_at are different" do + chunk = %{path: "hello-world", part_number: 1, inserted_at: DateTime.utc_now()} + + refute etag(chunk) == etag(%{chunk | path: "bye-for-now"}) + refute etag(chunk) == etag(%{chunk | part_number: 2}) + refute etag(chunk) == etag(%{chunk | inserted_at: DateTime.utc_now() |> DateTime.add(10)}) + end + end +end diff --git a/apps/object_storage/test/object_storage/object_test.exs b/apps/object_storage/test/object_storage/object_test.exs new file mode 100644 index 00000000..4c03b2ac --- /dev/null +++ b/apps/object_storage/test/object_storage/object_test.exs @@ -0,0 +1,17 @@ +defmodule Legendary.ObjectStorage.ObjectTest do + use Legendary.ObjectStorage.DataCase + + import Legendary.ObjectStorage.Object + + alias Legendary.ObjectStorage.Object + + describe "changeset/2" do + test "does not require body for multipart uploads" do + assert changeset(%Object{}, %{acl: "public_read", path: "test", uploads: "1"}) .valid? + end + + test "requires a body if single part upload" do + refute changeset(%Object{}, %{acl: "public_read", path: "test"}).valid? + end + end +end diff --git a/apps/object_storage/test/object_storage/objects_test.exs b/apps/object_storage/test/object_storage/objects_test.exs new file mode 100644 index 00000000..b225baa3 --- /dev/null +++ b/apps/object_storage/test/object_storage/objects_test.exs @@ -0,0 +1,97 @@ +defmodule Legendary.ObjectStorage.ObjectsTest do + use Legendary.ObjectStorage.DataCase + + alias Legendary.ObjectStorage.{Object, ObjectChunk, Objects} + + test "get_object/1 returns the object with given id" do + object = + %Object{path: "hello.txt"} + |> Repo.insert!() + assert Objects.get_object(object.path) == {:ok, object} + end + + describe "get_or_initialize_object/1" do + test "finds objects by path" do + object = + %Object{path: "hello.txt"} + |> Repo.insert!() + + assert %{path: "hello.txt"} = Objects.get_or_initialize_object(object.path) + end + + test "returns a blank Object if no object with the path exists" do + assert %{body: nil, acl: nil} = Objects.get_or_initialize_object("bad-path") + end + end + + test "update_object/2 with valid data updates the object" do + update_attrs = %{ + path: "test.txt", + body: "Hello, world!", + acl: "private" + } + + assert {:ok, %Object{} = object} = Objects.update_object(%Object{}, update_attrs) + assert object.path == "test.txt" + assert object.body == "Hello, world!" + assert object.acl == :private + end + + test "update_object/2 with invalid data returns error changeset" do + object = + %Object{path: "test.txt", body: "Hello, world!"} + |> Repo.insert!() + assert {:error, %Ecto.Changeset{}} = Objects.update_object(object, %{body: ""}) + assert {:ok, object} == Objects.get_object(object.path) + assert object.body == "Hello, world!" + end + + describe "put_chunk/2" do + test "adds a chunk to an object" do + result = Objects.put_chunk(%{path: "hello-world.txt"}, %{part_number: 1, body: "Hello,"}) + assert {:ok, %ObjectChunk{part_number: 1, body: "Hello,", path: "hello-world.txt"}} = result + end + end + + describe "finalized_chunked_upload/2" do + test "with contiguous chunks" do + object = Repo.insert!(%Object{path: "hello-world.txt", acl: :public_read}) + chunk1 = Repo.insert!(%ObjectChunk{path: "hello-world.txt", part_number: 1, body: "Hello, "}) + chunk2 = Repo.insert!(%ObjectChunk{path: "hello-world.txt", part_number: 2, body: "world!"}) + etags = [ObjectChunk.etag(chunk1), ObjectChunk.etag(chunk2)] + + assert { + :ok, + %{ + update_object_body: {updated_objects_count, nil}, + remove_chunks: {removed_chunks_count, nil} + } + } = Objects.finalize_chunked_upload(object, etags) + assert updated_objects_count == 1 + assert removed_chunks_count == 2 + assert {:ok, %Object{body: body}} = Objects.get_object("hello-world.txt") + assert body == "Hello, world!" + end + + test "with gap in chunks" do + object = Repo.insert!(%Object{path: "hello-world.txt", acl: :public_read}) + _chunk1 = Repo.insert!(%ObjectChunk{path: "hello-world.txt", part_number: 1, body: "Hell"}) + _chunk3 = Repo.insert!(%ObjectChunk{path: "hello-world.txt", part_number: 3, body: " world!"}) + + assert { + :error, + :check_chunks, + "Missing chunks for chunked upload. Aborting.", + _ + } = Objects.finalize_chunked_upload(object, []) + end + end + + test "delete_object/1 deletes the object" do + object = + %Object{path: "test.txt"} + |> Repo.insert!() + assert {:ok, %Object{path: "test.txt"}} = Objects.delete_object(object) + assert {:error, :not_found} = Objects.get_object(object.path) + end +end diff --git a/apps/object_storage/test/object_storage_test.exs b/apps/object_storage/test/object_storage_test.exs new file mode 100644 index 00000000..f4ab7b43 --- /dev/null +++ b/apps/object_storage/test/object_storage_test.exs @@ -0,0 +1,11 @@ +defmodule Legendary.ObjectStorage.Test do + use Legendary.ObjectStorage.DataCase + + alias Legendary.ObjectStorage + + describe "bucket_name/0" do + test "returns the bucket name" do + assert ObjectStorage.bucket_name() == "uploads" + end + end +end diff --git a/apps/object_storage/test/object_storage_web/controllers/chunked_upload_controller_test.exs b/apps/object_storage/test/object_storage_web/controllers/chunked_upload_controller_test.exs new file mode 100644 index 00000000..71da9c28 --- /dev/null +++ b/apps/object_storage/test/object_storage_web/controllers/chunked_upload_controller_test.exs @@ -0,0 +1,75 @@ +defmodule Legendary.ObjectStorageWeb.ChunkedUploadControllerTest do + use Legendary.ObjectStorageWeb.ConnCase + + alias Legendary.ObjectStorage.{Object, ObjectChunk, Repo} + + setup do + expect_signature_checks_and_pass() + end + + def post_request(conn, path, opts \\ []) do + content_type = Keyword.get(opts, :content_type, "text/plain") + acl = Keyword.get(opts, :acl, "public_read") + params = Keyword.get(opts, :params, %{}) + body = Keyword.get(opts, :body) + + conn + |> put_req_header("x-amz-acl", acl) + |> put_req_header("content-type", content_type) + |> post(Routes.chunked_upload_path(conn, :chunked_upload, path, params), body) + end + + describe "start" do + test "initiates an upload with proper variables", %{conn: conn} do + conn = post_request(conn, ["new-multipart-upload"], params: %{"uploads" => "1"}) + + assert response(conn, 200) + end + + test "return 400 Bad Request with a wrong ACL", %{conn: conn} do + conn = post_request(conn, ["new-multipart-upload"], acl: "wrong", params: %{"uploads" => "1"}) + + assert response(conn, 400) + end + end + + describe "finalize" do + test "finalizes an upload by pass", %{conn: conn} do + Repo.insert!(%Object{path: "new-multipart-upload", acl: :public_read}) + chunk = Repo.insert!(%ObjectChunk{path: "new-multipart-upload", part_number: 1}) + + conn = + post_request( + conn, + ["new-multipart-upload"], + params: %{"uploadId" => "1"}, + body: """ + + + + #{ObjectChunk.etag(chunk)} + 1 + + ... + + """ + ) + + assert response(conn, 200) + end + + test "returns a 400 Bad Request if chunks are missing", %{conn: conn} do + Repo.insert!(%Object{path: "new-multipart-upload", acl: :public_read}) + + conn = post_request(conn, ["new-multipart-upload"], params: %{"uploadId" => "1"}) + + assert response(conn, 400) + end + + test "returns a 404 if no such object exists", %{conn: conn} do + conn = post_request(conn, ["new-multipart-upload"], params: %{"uploadId" => "1"}) + + assert response(conn, 404) + end + end +end diff --git a/apps/object_storage/test/object_storage_web/controllers/upload_controller_test.exs b/apps/object_storage/test/object_storage_web/controllers/upload_controller_test.exs new file mode 100644 index 00000000..4abd7fef --- /dev/null +++ b/apps/object_storage/test/object_storage_web/controllers/upload_controller_test.exs @@ -0,0 +1,106 @@ +defmodule ObjectStorageWeb.UploadControllerTest do + use Legendary.ObjectStorageWeb.ConnCase + + alias Legendary.ObjectStorage.{Object, Repo} + + def put_request(conn, path, acl, body, params \\ %{}, content_type \\ "text/plain") do + conn + |> put_req_header("x-amz-acl", acl) + |> put_req_header("content-type", content_type) + |> put( + Routes.upload_path(conn, :put_object, path, params), + body + ) + end + + describe "show object" do + test "returns 404 if the object is private and sig check fails", %{conn: conn} do + expect_signature_checks_and_fail() + + Repo.insert!(%Object{path: "secret.txt", acl: :private, body: "Ssh!"}) + + conn = get(conn, Routes.upload_path(conn, :show, ["secret.txt"])) + + assert response(conn, 404) + assert response_content_type(conn, :xml) + end + + test "returns the object if the object is private but the sig check passes", %{conn: conn} do + expect_signature_checks_and_pass() + + Repo.insert!(%Object{path: "secret.txt", acl: :private, body: "Ssh!"}) + + conn = get(conn, Routes.upload_path(conn, :show, ["secret.txt"])) + + assert text_response(conn, 200) == "Ssh!" + end + end + + describe "put object" do + setup do + expect_signature_checks_and_pass() + end + + test "renders object when data is valid", %{conn: conn} do + conn = put_request(conn, ["test.txt"], "public_read", "Hello, world!") + assert text_response(conn, 200) == "" + + conn = get(conn, Routes.upload_path(conn, :show, ["test.txt"])) + + assert "Hello, world!" = text_response(conn, 200) + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = put_request(conn, ["test.txt"], "bad_acl", "Hello, world!") + assert response(conn, 400) =~ "InvalidArgument" + end + end + + describe "put chunk" do + setup [:create_object] + + setup do + expect_signature_checks_and_pass() + end + + test "can put a chunk if you give a part number", %{conn: conn, object: object} do + conn = put_request(conn, [object.path], "public_read", "Hello, world!", %{"partNumber" => 1, "uploadId" => 1}) + assert response(conn, 200) + end + + test "returns a 404 if the path is wrong", %{conn: conn} do + conn = put_request(conn, ["wrong"], "public_read", "Hello, world!", %{"partNumber" => 1, "uploadId" => 1}) + assert response(conn, 404) + end + + test "returns a 400 Bad Request if the body is missing", %{conn: conn, object: object} do + conn = put_request(conn, [object.path], "public_read", nil, %{"partNumber" => 1, "uploadId" => 1}) + assert response(conn, 400) =~ "InvalidArgument" + end + end + + describe "delete object" do + setup [:create_object] + + setup do + expect_signature_checks_and_pass() + end + + test "deletes chosen object", %{conn: conn} do + conn = delete(conn, Routes.upload_path(conn, :delete_object, ["test.txt"])) + assert response(conn, 204) + + conn = get(conn, Routes.upload_path(conn, :show, ["test.txt"])) + assert response(conn, 404) + end + + test "returns 404 if the path does not exist", %{conn: conn} do + conn = delete(conn, Routes.upload_path(conn, :delete_object, ["bad-path"])) + assert response(conn, 404) + end + end + + defp create_object(_) do + %{object: %Object{path: "test.txt"} |> Repo.insert!()} + end +end diff --git a/apps/object_storage/test/object_storage_web/plugs/check_signatures/signature_generator_test.exs b/apps/object_storage/test/object_storage_web/plugs/check_signatures/signature_generator_test.exs new file mode 100644 index 00000000..b8b6633a --- /dev/null +++ b/apps/object_storage/test/object_storage_web/plugs/check_signatures/signature_generator_test.exs @@ -0,0 +1,46 @@ +defmodule Legendary.ObjectStorageWeb.CheckSignatures.SignatureGeneratorTest do + use Legendary.ObjectStorageWeb.ConnCase + + import Legendary.ObjectStorageWeb.CheckSignatures.SignatureGenerator + + alias ExAws.S3 + + require IEx + + describe "correct_signature_for_conn/1"do + test "handles signature in authorization header" do + conn = + "PUT" + |> build_conn("/uploads/sig-test.txt", %{"path" => ["sig-test.txt"]}) + |> put_req_header("host", "localhost:4000") + |> put_req_header("x-amz-date", "20211015T000000Z") + |> put_req_header("authorization", "AWS4-HMAC-SHA256 SignedHeaders=host;x-amz-date") + |> assign(:body, "") + + assert {:ok, sig} = correct_signature_for_conn(conn) + assert sig == "964cf3b50a10e020dee639986b2423118144e0ac4371f45a6ecf75adb043712b" + end + + test "handles presigned url" do + {:ok, url} = S3.presigned_url(ExAws.Config.new(:s3), :put, "uploads", "hello-world.txt") + target_sig = + url + |> URI.parse() + |> Map.get(:query) + |> URI.decode_query() + |> Map.get("X-Amz-Signature") + + conn = + "PUT" + |> build_conn( + url, + %{"path" => ["hello-world.txt"]} + ) + |> assign(:body, nil) + |> put_req_header("host", "localhost:4000") + + assert {:ok, sig} = correct_signature_for_conn(conn) + assert sig == target_sig + end + end +end diff --git a/apps/object_storage/test/object_storage_web/plugs/check_signatures_test.exs b/apps/object_storage/test/object_storage_web/plugs/check_signatures_test.exs new file mode 100644 index 00000000..61514c88 --- /dev/null +++ b/apps/object_storage/test/object_storage_web/plugs/check_signatures_test.exs @@ -0,0 +1,44 @@ +defmodule Legendary.ObjectStorageWeb.CheckSignaturesTest do + use Legendary.ObjectStorageWeb.ConnCase + + import Legendary.ObjectStorageWeb.CheckSignatures + import Mox + + alias Legendary.ObjectStorageWeb.CheckSignatures.MockSignatureGenerator + + test "init/1 returns opts", do: assert init(nil) == nil + + describe "call/2" do + test "with a good signature in header it continues", %{conn: conn} do + MockSignatureGenerator + |> expect(:correct_signature_for_conn, fn _conn -> {:ok, "good-sig"} end) + |> expect(:parse_authorization_header, fn _ -> %{"Signature" => "good-sig"} end) + + refute call(conn, nil).halted + end + + test "with a good signature in query params it continues", %{conn: conn} do + MockSignatureGenerator + |> expect(:correct_signature_for_conn, fn _conn -> {:ok, "good-sig"} end) + + conn = %{conn | query_params: %{"X-Amz-Signature" => "good-sig"}} + + refute call(conn, nil).halted + end + + test "with a bad signature it halts", %{conn: conn} do + MockSignatureGenerator + |> expect(:correct_signature_for_conn, fn _conn -> {:ok, "good-sig"} end) + + conn = %{conn | query_params: %{"X-Amz-Signature" => "bad-sig"}} + + assert call(conn, nil).halted + end + + test "with an expired request it halts", %{conn: conn} do + conn = %{conn | query_params: %{"X-Amz-Date" => "19000101T000000Z", "X-Amz-Expires" => 3600}} + + assert call(conn, nil).halted + end + end +end diff --git a/apps/object_storage/test/object_storage_web/views/error_view_test.exs b/apps/object_storage/test/object_storage_web/views/error_view_test.exs new file mode 100644 index 00000000..ac169311 --- /dev/null +++ b/apps/object_storage/test/object_storage_web/views/error_view_test.exs @@ -0,0 +1,14 @@ +defmodule Legendary.ObjectStorageWeb.ErrorViewTest do + use Legendary.ObjectStorageWeb.ConnCase, async: true + + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + + test "renders 404.html" do + assert render_to_string(Legendary.ObjectStorageWeb.ErrorView, "404.html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(Legendary.ObjectStorageWeb.ErrorView, "500.html", []) == "Internal Server Error" + end +end diff --git a/apps/object_storage/test/object_storage_web/views/layout_view_test.exs b/apps/object_storage/test/object_storage_web/views/layout_view_test.exs new file mode 100644 index 00000000..33c5e339 --- /dev/null +++ b/apps/object_storage/test/object_storage_web/views/layout_view_test.exs @@ -0,0 +1,8 @@ +defmodule Legendary.ObjectStorageWeb.LayoutViewTest do + use Legendary.ObjectStorageWeb.ConnCase, async: true + + # When testing helpers, you may want to import Phoenix.HTML and + # use functions such as safe_to_string() to convert the helper + # result into an HTML string. + # import Phoenix.HTML +end diff --git a/apps/object_storage/test/object_storage_web/views/page_view_test.exs b/apps/object_storage/test/object_storage_web/views/page_view_test.exs new file mode 100644 index 00000000..edac771e --- /dev/null +++ b/apps/object_storage/test/object_storage_web/views/page_view_test.exs @@ -0,0 +1,3 @@ +defmodule Legendary.ObjectStorageWeb.PageViewTest do + use Legendary.ObjectStorageWeb.ConnCase, async: true +end diff --git a/apps/object_storage/test/seed_sets/.keep b/apps/object_storage/test/seed_sets/.keep new file mode 100644 index 00000000..e69de29b diff --git a/apps/object_storage/test/seed_sets/test_end_to_end_test.exs b/apps/object_storage/test/seed_sets/test_end_to_end_test.exs new file mode 100644 index 00000000..e69de29b diff --git a/apps/object_storage/test/support/channel_case.ex b/apps/object_storage/test/support/channel_case.ex new file mode 100644 index 00000000..0bef4bd7 --- /dev/null +++ b/apps/object_storage/test/support/channel_case.ex @@ -0,0 +1,38 @@ +defmodule Legendary.ObjectStorageWeb.ChannelCase do + @moduledoc """ + This module defines the test case to be used by + channel tests. + + Such tests rely on `Phoenix.ChannelTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use Legendary.ObjectStorageWeb.ChannelCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + # Import conveniences for testing with channels + import Phoenix.ChannelTest + import Legendary.ObjectStorageWeb.ChannelCase + + # The default endpoint for testing + @endpoint Legendary.ObjectStorageWeb.Endpoint + end + end + + setup tags do + pid = Sandbox.start_owner!(Legendary.ObjectStorage.Repo, shared: not tags[:async]) + on_exit(fn -> Sandbox.stop_owner(pid) end) + :ok + end +end diff --git a/apps/object_storage/test/support/conn_case.ex b/apps/object_storage/test/support/conn_case.ex new file mode 100644 index 00000000..97ec1962 --- /dev/null +++ b/apps/object_storage/test/support/conn_case.ex @@ -0,0 +1,43 @@ +defmodule Legendary.ObjectStorageWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `us Legendary.ObjectStorageWeb.ConnCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + alias Legendary.ObjectStorage.Repo + + using do + quote do + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import Legendary.ObjectStorageWeb.ConnCase + import Legendary.ObjectStorageWeb.SignatureTestingUtilities + + alias Legendary.ObjectStorageWeb.Router.Helpers, as: Routes + + # The default endpoint for testing + @endpoint Legendary.ObjectStorageWeb.Endpoint + end + end + + setup tags do + pid = Sandbox.start_owner!(Repo, shared: not tags[:async]) + on_exit(fn -> Sandbox.stop_owner(pid) end) + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/apps/object_storage/test/support/data_case.ex b/apps/object_storage/test/support/data_case.ex new file mode 100644 index 00000000..b5a0aa2b --- /dev/null +++ b/apps/object_storage/test/support/data_case.ex @@ -0,0 +1,53 @@ +defmodule Legendary.ObjectStorage.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use Legendary.ObjectStorage.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + alias Legendary.ObjectStorage.Repo + + import Ecto + import Ecto.Changeset + import Ecto.Query + import Legendary.ObjectStorage.DataCase + end + end + + setup tags do + pid = Sandbox.start_owner!(Legendary.ObjectStorage.Repo, shared: not tags[:async]) + on_exit(fn -> Sandbox.stop_owner(pid) end) + :ok + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end +end diff --git a/apps/object_storage/test/support/signature_testing_utilities.ex b/apps/object_storage/test/support/signature_testing_utilities.ex new file mode 100644 index 00000000..e5e289c3 --- /dev/null +++ b/apps/object_storage/test/support/signature_testing_utilities.ex @@ -0,0 +1,29 @@ +defmodule Legendary.ObjectStorageWeb.SignatureTestingUtilities do + @moduledoc """ + Utilities that make it easier to test controller actions which require auth + signatures. + """ + import Mox + + alias Legendary.ObjectStorageWeb.CheckSignatures.MockSignatureGenerator + + def expect_signature_checks_and_pass do + verify_on_exit!() + + MockSignatureGenerator + |> expect(:correct_signature_for_conn, fn _conn -> {:ok, "good-sig"} end) + |> expect(:parse_authorization_header, fn _ -> %{"Signature" => "good-sig"} end) + + :ok + end + + def expect_signature_checks_and_fail do + verify_on_exit!() + + MockSignatureGenerator + |> expect(:correct_signature_for_conn, fn _conn -> {:ok, "good-sig"} end) + |> expect(:parse_authorization_header, fn _ -> %{"Signature" => "bad-sig"} end) + + :ok + end +end diff --git a/apps/object_storage/test/test_helper.exs b/apps/object_storage/test/test_helper.exs new file mode 100644 index 00000000..c6e83b09 --- /dev/null +++ b/apps/object_storage/test/test_helper.exs @@ -0,0 +1,7 @@ +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(Legendary.ObjectStorage.Repo, :manual) + +Mox.defmock( + Legendary.ObjectStorageWeb.CheckSignatures.MockSignatureGenerator, + for: Legendary.ObjectStorageWeb.CheckSignatures.SignatureGenerator +) diff --git a/config/admin.exs b/config/admin.exs index 1dd8911c..2e8f22da 100644 --- a/config/admin.exs +++ b/config/admin.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :kaffy, otp_app: :admin, diff --git a/config/config.exs b/config/config.exs index 28c447d3..bc791a33 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config [ {:admin, Legendary.Admin, false}, @@ -6,6 +6,7 @@ use Mix.Config {:core, Legendary.AuthWeb, false}, {:content, Legendary.Content, false}, {:core, Legendary.CoreWeb, false}, + {:object_storage, Legendary.ObjectStorageWeb, false} ] |> Enum.map(fn {otp_app, module, start_server} -> endpoint = Module.concat(module, "Endpoint") @@ -25,6 +26,7 @@ end) {:app, App.Repo}, {:content, Legendary.Content.Repo}, {:core, Legendary.Core.Repo}, + {:object_storage, Legendary.ObjectStorage.Repo} ] |> Enum.map(fn {otp_app, repo} -> @@ -32,8 +34,7 @@ end) ecto_repos: [repo], generators: [context_app: otp_app] - config otp_app, repo, - pool: Legendary.Core.SharedDBConnectionPool + config otp_app, repo, pool: Legendary.Core.SharedDBConnectionPool end) config :core, :pow, @@ -60,27 +61,27 @@ config :phoenix, :json_library, Jason config :linguist, pluralization_key: :count config :content, - Oban, - repo: Legendary.Content.Repo, - queues: [default: 10], - crontab: [ - {"0 * * * *", Legendary.Content.Sitemaps}, - ] + Oban, + repo: Legendary.Content.Repo, + queues: [default: 10], + crontab: [ + {"0 * * * *", Legendary.Content.Sitemaps} + ] config :app, - Oban, - repo: App.Repo, - queues: [default: 10], - crontab: [ - ] + Oban, + repo: App.Repo, + queues: [default: 10], + crontab: [] -config :mnesia, dir: to_charlist(Path.expand("./priv/mnesia@#{Kernel.node}")) +config :mnesia, dir: to_charlist(Path.expand("./priv/mnesia@#{Kernel.node()}")) # Feature flags config :fun_with_flags, :cache, enabled: true, - ttl: 300 # seconds + # seconds + ttl: 300 config :fun_with_flags, :persistence, adapter: FunWithFlags.Store.Persistent.Ecto, @@ -92,7 +93,7 @@ config :fun_with_flags, :cache_bust_notifications, client: App.PubSub # Notifications can also be disabled, which will also remove the Redis/Redix dependency -config :fun_with_flags, :cache_bust_notifications, [enabled: false] +config :fun_with_flags, :cache_bust_notifications, enabled: false import_config "email_styles.exs" import_config "admin.exs" diff --git a/config/dev.exs b/config/dev.exs index 99633450..dc877d46 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # For development, we disable any cache and enable # debugging and code reloading. @@ -12,6 +12,7 @@ use Mix.Config {:core, Legendary.AuthWeb}, {:content, Legendary.Content}, {:core, Legendary.CoreWeb}, + {:object_storage, Legendary.ObjectStorageWeb} ] |> Enum.map(fn {otp_app, module} -> config otp_app, Module.concat(module, "Endpoint"), @@ -46,7 +47,8 @@ end) {:admin, Legendary.Admin.Repo}, {:app, App.Repo}, {:content, Legendary.Content.Repo}, - {:core, Legendary.Core.Repo} + {:core, Legendary.Core.Repo}, + {:object_storage, Legendary.ObjectStorage.Repo} ] |> Enum.map(fn {otp_app, repo} -> config otp_app, repo, @@ -63,10 +65,17 @@ config :core, Legendary.CoreMailer, adapter: Bamboo.LocalAdapter config :libcluster, topologies: [ erlang_hosts: [ - strategy: Elixir.Cluster.Strategy.Gossip, + strategy: Elixir.Cluster.Strategy.Gossip ] ] +# Use this configuration to use Waffle with our internal object storage engine +# that simulates S3 +config :waffle, + storage: Waffle.Storage.S3, + bucket: "uploads", + asset_host: "http://localhost:4000" + # ## SSL Support # # In order to use HTTPS in development, a self-signed @@ -90,3 +99,15 @@ config :libcluster, # If desired, both `http:` and `https:` keys can be # configured to run both http and https servers on # different ports. + +config :object_storage, + bucket_name: "uploads" + +config :ex_aws, + access_key_id: "dev-test-access-key-id", + secret_access_key: "dev-test-secret-access-key" + +config :ex_aws, :s3, + scheme: "http://", + host: "localhost", + port: 4000 diff --git a/config/e2e.exs b/config/e2e.exs index 8e70ec83..204f225a 100644 --- a/config/e2e.exs +++ b/config/e2e.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # Start with test config import_config "test.exs" diff --git a/config/email_styles.exs b/config/email_styles.exs index f983382d..bc00ff57 100644 --- a/config/email_styles.exs +++ b/config/email_styles.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :core, :email, %{ styles: %{ diff --git a/config/prod.exs b/config/prod.exs index 54c83fd4..2ab1f217 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # For production, don't forget to configure the url host # to something meaningful, Phoenix uses this information @@ -18,6 +18,7 @@ signing_salt = System.get_env("LIVE_VIEW_SIGNING_SALT") {:app, AppWeb, true}, {:content, ContentWeb, false}, {:core, Legendary.CoreWeb, false}, + {:object_storage, Legendary.ObjectStorageWeb, false} ] |> Enum.map(fn {otp_app, module, start_server} -> endpoint = Module.concat(module, "Endpoint") @@ -28,17 +29,19 @@ signing_salt = System.get_env("LIVE_VIEW_SIGNING_SALT") [] end - config otp_app, endpoint, [ - url: [host: "example.com", port: 80], - http: [ - port: String.to_integer(System.get_env("PORT") || "4000"), - transport_options: [socket_opts: [:inet6]] - ], - secret_key_base: secret_key_base, - pubsub_server: App.PubSub, - live_view: [signing_salt: signing_salt], - server: start_server - ] ++ extra_opts + config otp_app, + endpoint, + [ + url: [host: "example.com", port: 80], + http: [ + port: String.to_integer(System.get_env("PORT") || "4000"), + transport_options: [socket_opts: [:inet6]] + ], + secret_key_base: secret_key_base, + pubsub_server: App.PubSub, + live_view: [signing_salt: signing_salt], + server: start_server + ] ++ extra_opts end) # ## Using releases (Elixir v1.9+) @@ -57,7 +60,8 @@ database_url = System.get_env("DATABASE_URL") {:admin, Legendary.Admin.Repo}, {:app, App.Repo}, {:content, Legendary.Content.Repo}, - {:core, Legendary.Core.Repo} + {:core, Legendary.Core.Repo}, + {:object_storage, Legendary.ObjectStorage.Repo} ] |> Enum.map(fn {otp_app, repo} -> config otp_app, repo, @@ -91,7 +95,29 @@ config :libcluster, kubernetes_node_basename: System.get_env("NAME", "legendary"), kubernetes_selector: "app=#{System.get_env("NAME", "legendary")}", kubernetes_namespace: System.get_env("NAMESPACE", "legendary"), - polling_interval: 10_000]]] + polling_interval: 10_000 + ] + ] + ] + +# Use this configuration to use Waffle with our internal object storage engine +# that simulates S3 +config :waffle, + storage: Waffle.Storage.S3, + bucket: "uploads" + asset_host: "https://#{System.get_env("HOSTNAME")}" + +config :object_storage, + bucket_name: "uploads" + +config :ex_aws, + access_key_id: {:system, "OBJECT_STORAGE_ACCESS_KEY_ID"}, + secret_access_key: {:system, "OBJECT_STORAGE_SECRET_ACCESS_KEY"} + +config :ex_aws, :s3, + scheme: "https://", + host: {:system, "HOSTNAME"} + # ## Using releases (Elixir v1.9+) # diff --git a/config/test.exs b/config/test.exs index a35a8174..9ea47e36 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # We don't run a server during test. If one is required, # you can enable the server option below. @@ -8,6 +8,7 @@ use Mix.Config {:core, Legendary.AuthWeb}, {:content, Legendary.Content}, {:core, Legendary.CoreWeb}, + {:object_storage, Legendary.ObjectStorageWeb} ] |> Enum.map(fn {otp_app, module} -> config otp_app, Module.concat(module, "Endpoint"), @@ -25,7 +26,8 @@ end) {:admin, Legendary.Admin.Repo}, {:app, App.Repo}, {:content, Legendary.Content.Repo}, - {:core, Legendary.Core.Repo} + {:core, Legendary.Core.Repo}, + {:object_storage, Legendary.ObjectStorage.Repo} ] |> Enum.map(fn {otp_app, repo} -> config otp_app, repo, @@ -43,3 +45,22 @@ config :content, Oban, crontab: false, queues: false, plugins: false config :logger, level: :warn config :libcluster, topologies: [] + +config :waffle, + storage: Waffle.Storage.Local, + storage_dir_prefix: "priv/test/static/", + asset_host: "http://localhost:4000" + +config :object_storage, + bucket_name: "uploads" + +config :ex_aws, + access_key_id: "test-access-key-id", + secret_access_key: "test-secret-access-key" + +config :ex_aws, :s3, + scheme: "http://", + host: "localhost", + port: 4000 + +config :object_storage, :signature_generator, Legendary.ObjectStorageWeb.CheckSignatures.MockSignatureGenerator diff --git a/infrastructure_templates/kube.yaml.dot b/infrastructure_templates/kube.yaml.dot index 98cb4e7a..e2bbb408 100644 --- a/infrastructure_templates/kube.yaml.dot +++ b/infrastructure_templates/kube.yaml.dot @@ -63,6 +63,16 @@ spec: secretKeyRef: name: legendary key: live-view-signing-salt + - name: OBJECT_STORAGE_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: legendary + key: object-storage-access-key-id + - name: OBJECT_STORAGE_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: legendary + key: object-storage-secret-access-key - name: SMTP_HOST valueFrom: secretKeyRef: diff --git a/mix.lock b/mix.lock index 71dde73d..5773106f 100644 --- a/mix.lock +++ b/mix.lock @@ -3,6 +3,7 @@ "bamboo_smtp": {:hex, :bamboo_smtp, "3.0.0", "b7f0c371af96a1cb7131908918b02abb228f9db234910bf10cf4fb177c083259", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "77cb1fa3076b24109e54df622161fe1e5619376b4ecf86d8b99b46f327acc49f"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "10f658be786bd2daaadcd45cc5b598da01d5bbc313da4d0e3efb2d6a511d896d"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "castore": {:hex, :castore, "0.1.11", "c0665858e0e1c3e8c27178e73dffea699a5b28eb72239a3b2642d208e8594914", [:mix], [], "hexpm", "91b009ba61973b532b84f7c09ce441cba7aa15cb8b006cf06c6f4bba18220081"}, "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, "cldr_utils": {:hex, :cldr_utils, "2.16.0", "5abd1835151e264f6f9a285ab8c7419954a45eec5ca5a356dea592faa23e80b9", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3ef5dc0fdfe566a5a4b8bda726cf760ebada69c0600affc4cb02b5e8ae7f7b47"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, @@ -22,9 +23,13 @@ "ecto_sql": {:hex, :ecto_sql, "3.7.0", "2fcaad4ab0c8d76a5afbef078162806adbe709c04160aca58400d5cbbe8eeac6", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a26135dfa1d99bf87a928c464cfa25bba6535a4fe761eefa56077a4febc60f70"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "esbuild": {:hex, :esbuild, "0.3.1", "bf6a3783f8677aa93e8e6ee04b79eeceadb29e07255941fab7e50f1e3527f4a8", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "342ccd0eb2c64211326580189389d52cdf0f16f5ca22bc0267a66357e269a14a"}, + "ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.3.0", "5dfe50116bad048240bae7cd9418bfe23296542ff72a01b9138113a1cd31451c", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0b13b11478825d62d2f6e57ae763695331be06f2216468f31bb304316758b096"}, "ex_cldr": {:hex, :ex_cldr, "2.23.2", "76c51b722cefdcd1a13eb5e7c7f4da5b9acfd64ff054424a977ff6e2d6a78981", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.15", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d9ce03c8d3fdc7ab751bdb2be742b6972f94adc856d51dfe5bb06a51ac96b8f4"}, "ex_doc": {:hex, :ex_doc, "0.25.2", "4f1cae793c4d132e06674b282f1d9ea3bf409bcca027ddb2fe177c4eed6a253f", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5b0c172e87ac27f14dfd152d52a145238ec71a95efbf29849550278c58a393d6"}, "ex_prompt": {:hex, :ex_prompt, "0.2.0", "4030424e9a7710e1939d81eea4a82af2e0a1826065adb28d59bc01e919af4a60", [:mix], [], "hexpm", "220ac023d87d529457b87c9db4b40ce542bff93ae2de16c582808c6822dfe3e8"}, + "ex_url": {:hex, :ex_url, "1.3.1", "c39b2227c77342ca76f0a4d4d27858726abfebad463023264d3ba4d9549bbf4c", [:mix], [{:ex_cldr, "~> 2.18", [hex: :ex_cldr, repo: "hexpm", optional: true]}, {:ex_phone_number, "~> 0.1", [hex: :ex_phone_number, repo: "hexpm", optional: true]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b4e0d385e2c172643964dc6766c7b23fea85561af1c374759438b07faa9a801d"}, "excoveralls": {:hex, :excoveralls, "0.14.2", "f9f5fd0004d7bbeaa28ea9606251bb643c313c3d60710bad1f5809c845b748f0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ca6fd358621cb4d29311b29d4732c4d47dac70e622850979bc54ed9a3e50f3e1"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"}, @@ -53,11 +58,12 @@ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mochiweb": {:hex, :mochiweb, "2.22.0", "f104d6747c01a330c38613561977e565b788b9170055c5241ac9dd6e4617cba5", [:rebar3], [], "hexpm", "cbbd1fd315d283c576d1c8a13e0738f6dafb63dc840611249608697502a07655"}, "mock": {:hex, :mock, "0.3.6", "e810a91fabc7adf63ab5fdbec5d9d3b492413b8cda5131a2a8aa34b4185eb9b4", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "bcf1d0a6826fb5aee01bae3d74474669a3fa8b2df274d094af54a25266a1ebd2"}, + "mox": {:hex, :mox, "1.0.0", "4b3c7005173f47ff30641ba044eb0fe67287743eec9bd9545e37f3002b0a9f8b", [:mix], [], "hexpm", "201b0a20b7abdaaab083e9cf97884950f8a30a1350a1da403b3145e213c6f4df"}, "neotomex": {:hex, :neotomex, "0.1.7", "64f76513653aa87ea7abdde0fd600e56955d838020a13d88f2bf334c88ac3e7a", [:mix], [], "hexpm", "4b87b8f614d1cd89dc8ba80ba0e559bedb3ebf6f6d74cd774fcfdd215e861445"}, "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "oban": {:hex, :oban, "2.9.1", "e92a96d4ddc3731816e7c6463b8f50f9bfaadc560686a23f10a5aac0fbeb7572", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f734b1fe0c2320a624eb03cc9d0036bd41ee6248f332805c68182e7de0a43514"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "phoenix": {:hex, :phoenix, "1.6.0", "7b85023f7ddef9a5c70909a51cc37c8b868b474d853f90f4280efd26b0e7cce5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "52ffdd31f2daeb399b2e1eb57d468f99a1ad6eee5d8ea19d2353492f06c9fc96"}, + "phoenix": {:hex, :phoenix, "1.6.2", "6cbd5c8ed7a797f25a919a37fafbc2fb1634c9cdb12a4448d7a5d0b26926f005", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bbee475acae0c3abc229b7f189e210ea788e63bd168e585f60c299a4b2f9133"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.0.4", "232d41884fe6a9c42d09f48397c175cd6f0d443aaa34c7424da47604201df2e1", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ce17fd3cf815b2ed874114073e743507704b1f5288bb03c304a77458485efc8b"}, "phoenix_html_sanitizer": {:hex, :phoenix_html_sanitizer, "1.1.0", "ea9e1162217621208ba6b2951a24abe2c06b39347f65c22c31312f9f5ac0fa75", [:mix], [{:html_sanitize_ex, "~> 1.1", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "089f28f0592d58f7cf1f032b89c13e873dc73c77a2ccf3386aee976c6ff077c9"}, @@ -78,12 +84,15 @@ "slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "94884f84783fc1ba027aba8fe8a7dae4aad78c98e9f9c76667ec3471585c08c6"}, + "sweet_xml": {:hex, :sweet_xml, "0.7.1", "a2cac8e2101237e617dfa9d427d44b8aff38ba6294f313ffb4667524d6b71b98", [:mix], [], "hexpm", "8bc7b7b584a6a87113071d0d2fd39fe2251cf2224ecaeed7093bdac1b9c1555f"}, + "swoosh": {:hex, :swoosh, "1.5.0", "2be4cfc1be10f2203d1854c85b18d8c7be0321445a782efd53ef0b2b88f03ce4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b53891359e3ddca263ece784051243de84c9244c421a0dee1bff1d52fc5ca420"}, "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "timex": {:hex, :timex, "3.7.6", "502d2347ec550e77fdf419bc12d15bdccd31266bb7d925b30bf478268098282f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "a296327f79cb1ec795b896698c56e662ed7210cc9eb31f0ab365eb3a62e2c589"}, "tzdata": {:hex, :tzdata, "1.1.0", "72f5babaa9390d0f131465c8702fa76da0919e37ba32baa90d93c583301a8359", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "18f453739b48d3dc5bcf0e8906d2dc112bb40baafe2c707596d89f3c8dd14034"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "waffle": {:hex, :waffle, "1.1.5", "11b8b41c9dc46a21c8e1e619e1e9048d18d166b57b33d1fada8e11fcd4e678b3", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "68e6f92b457b13c71e33cc23f7abb60446a01515dc6618b7d493d8cd466b1f39"}, "xml_builder": {:hex, :xml_builder, "2.2.0", "cc5f1eeefcfcde6e90a9b77fb6c490a20bc1b856a7010ce6396f6da9719cbbab", [:mix], [], "hexpm", "9d66d52fb917565d358166a4314078d39ef04d552904de96f8e73f68f64a62c9"}, "yamerl": {:hex, :yamerl, "0.8.1", "07da13ffa1d8e13948943789665c62ccd679dfa7b324a4a2ed3149df17f453a4", [:rebar3], [], "hexpm", "96cb30f9d64344fed0ef8a92e9f16f207de6c04dfff4f366752ca79f5bceb23f"}, "yaml_elixir": {:hex, :yaml_elixir, "2.8.0", "c7ff0034daf57279c2ce902788ce6fdb2445532eb4317e8df4b044209fae6832", [:mix], [{:yamerl, "~> 0.8", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4b674bd881e373d1ac6a790c64b2ecb69d1fd612c2af3b22de1619c15473830b"},