From 9f407e91d3d508f1fadd77b47e18906f53a21641 Mon Sep 17 00:00:00 2001 From: Robert Prehn Date: Tue, 19 Oct 2021 19:28:25 +0000 Subject: [PATCH] feat: Add object_storage engine and social media preview image generation --- .gitignore | 3 + apps/admin/lib/kaffy/editor_extension.ex | 7 +- apps/admin/mix.exs | 2 +- apps/app/assets/css/admin.css | 5 + apps/app/assets/js/admin.js | 4 + .../assets/js/{ => admin}/content-editor.js | 4 +- apps/app/assets/js/admin/preview-image.js | 140 + apps/app/assets/package-lock.json | 2494 ++++++++++++++++- apps/app/assets/package.json | 1 + .../social-media-preview-background.png | Bin 0 -> 127935 bytes apps/app/assets/webpack.config.js | 2 +- apps/app/lib/app_web/endpoint.ex | 2 +- apps/app/lib/app_web/router.ex | 1 + .../app_web/templates/layout/_social.html.eex | 4 +- apps/app/lib/app_web/views/layout_view.ex | 16 + apps/app/mix.exs | 3 +- .../test/app_web/views/layout_view_test.exs | 16 + apps/content/lib/content/post_admin.ex | 11 +- .../lib/content/posts/preview_images.ex | 30 + .../uploaders/social_media_preview.ex | 18 + apps/content/mix.exs | 2 +- .../content/posts/preview_images_test.exs | 36 + .../uploaders/social_media_preview_test.exs | 17 + apps/core/lib/core_web/base64_uploads.ex | 48 + apps/core/lib/core_web/views/helpers.ex | 2 +- apps/core/mix.exs | 7 +- .../test/core_web/base64_uploads_test.exs | 38 + apps/object_storage/.formatter.exs | 5 + apps/object_storage/.gitignore | 33 + apps/object_storage/README.md | 3 + apps/object_storage/cypress.json | 3 + .../cypress/fixtures/example.json | 5 + .../cypress/integration/example_spec.js | 5 + apps/object_storage/cypress/plugins/index.js | 22 + .../cypress/support/commands.js | 36 + apps/object_storage/cypress/support/index.js | 29 + apps/object_storage/file.dmg | 0 apps/object_storage/lib/object_storage.ex | 11 + .../lib/object_storage/application.ex | 35 + .../lib/object_storage/object.ex | 35 + .../lib/object_storage/object_chunk.ex | 28 + .../lib/object_storage/objects.ex | 145 + .../object_storage/lib/object_storage/repo.ex | 5 + apps/object_storage/lib/object_storage_web.ex | 105 + .../controllers/chunked_upload_controller.ex | 81 + .../controllers/fallback_controller.ex | 16 + .../controllers/upload_controller.ex | 106 + .../lib/object_storage_web/endpoint.ex | 40 + .../lib/object_storage_web/gettext.ex | 24 + .../lib/object_storage_web/helpers.ex | 34 + .../plugs/check_signatures.ex | 70 + .../check_signatures/signature_generator.ex | 103 + .../lib/object_storage_web/router.ex | 39 + .../lib/object_storage_web/routes.ex | 21 + .../lib/object_storage_web/telemetry.ex | 74 + .../templates/layout/app.html.heex | 5 + .../templates/layout/live.html.heex | 11 + .../templates/layout/root.html.heex | 30 + .../templates/page/index.html.heex | 41 + .../views/changeset_view.ex | 19 + .../object_storage_web/views/error_helpers.ex | 47 + .../object_storage_web/views/error_view.ex | 16 + .../object_storage_web/views/layout_view.ex | 7 + .../lib/object_storage_web/views/page_view.ex | 3 + .../object_storage_web/views/upload_view.ex | 62 + apps/object_storage/mix.exs | 80 + .../priv/repo/migrations/.formatter.exs | 4 + .../20210928031038_create_storage_objects.exs | 15 + ...1005220041_create_storage_object_chunk.exs | 15 + apps/object_storage/priv/repo/seeds.exs | 11 + apps/object_storage/test.dmg | 0 .../test/object_storage/object_chunk_test.exs | 33 + .../test/object_storage/object_test.exs | 17 + .../test/object_storage/objects_test.exs | 97 + .../test/object_storage_test.exs | 11 + .../chunked_upload_controller_test.exs | 75 + .../controllers/upload_controller_test.exs | 106 + .../signature_generator_test.exs | 46 + .../plugs/check_signatures_test.exs | 44 + .../views/error_view_test.exs | 14 + .../views/layout_view_test.exs | 8 + .../views/page_view_test.exs | 3 + apps/object_storage/test/seed_sets/.keep | 0 .../test/seed_sets/test_end_to_end_test.exs | 0 .../test/support/channel_case.ex | 38 + apps/object_storage/test/support/conn_case.ex | 43 + apps/object_storage/test/support/data_case.ex | 53 + .../support/signature_testing_utilities.ex | 29 + apps/object_storage/test/test_helper.exs | 7 + config/admin.exs | 2 +- config/config.exs | 35 +- config/dev.exs | 27 +- config/e2e.exs | 2 +- config/email_styles.exs | 2 +- config/prod.exs | 54 +- config/test.exs | 25 +- infrastructure_templates/kube.yaml.dot | 10 + mix.lock | 11 +- 98 files changed, 5075 insertions(+), 109 deletions(-) create mode 100644 apps/app/assets/css/admin.css create mode 100644 apps/app/assets/js/admin.js rename apps/app/assets/js/{ => admin}/content-editor.js (90%) create mode 100644 apps/app/assets/js/admin/preview-image.js create mode 100644 apps/app/assets/static/images/social-media-preview-background.png create mode 100644 apps/content/lib/content/posts/preview_images.ex create mode 100644 apps/content/lib/content_web/uploaders/social_media_preview.ex create mode 100644 apps/content/test/content/posts/preview_images_test.exs create mode 100644 apps/content/test/content_web/uploaders/social_media_preview_test.exs create mode 100644 apps/core/lib/core_web/base64_uploads.ex create mode 100644 apps/core/test/core_web/base64_uploads_test.exs create mode 100644 apps/object_storage/.formatter.exs create mode 100644 apps/object_storage/.gitignore create mode 100644 apps/object_storage/README.md create mode 100644 apps/object_storage/cypress.json create mode 100644 apps/object_storage/cypress/fixtures/example.json create mode 100644 apps/object_storage/cypress/integration/example_spec.js create mode 100644 apps/object_storage/cypress/plugins/index.js create mode 100644 apps/object_storage/cypress/support/commands.js create mode 100644 apps/object_storage/cypress/support/index.js create mode 100644 apps/object_storage/file.dmg create mode 100644 apps/object_storage/lib/object_storage.ex create mode 100644 apps/object_storage/lib/object_storage/application.ex create mode 100644 apps/object_storage/lib/object_storage/object.ex create mode 100644 apps/object_storage/lib/object_storage/object_chunk.ex create mode 100644 apps/object_storage/lib/object_storage/objects.ex create mode 100644 apps/object_storage/lib/object_storage/repo.ex create mode 100644 apps/object_storage/lib/object_storage_web.ex create mode 100644 apps/object_storage/lib/object_storage_web/controllers/chunked_upload_controller.ex create mode 100644 apps/object_storage/lib/object_storage_web/controllers/fallback_controller.ex create mode 100644 apps/object_storage/lib/object_storage_web/controllers/upload_controller.ex create mode 100644 apps/object_storage/lib/object_storage_web/endpoint.ex create mode 100644 apps/object_storage/lib/object_storage_web/gettext.ex create mode 100644 apps/object_storage/lib/object_storage_web/helpers.ex create mode 100644 apps/object_storage/lib/object_storage_web/plugs/check_signatures.ex create mode 100644 apps/object_storage/lib/object_storage_web/plugs/check_signatures/signature_generator.ex create mode 100644 apps/object_storage/lib/object_storage_web/router.ex create mode 100644 apps/object_storage/lib/object_storage_web/routes.ex create mode 100644 apps/object_storage/lib/object_storage_web/telemetry.ex create mode 100644 apps/object_storage/lib/object_storage_web/templates/layout/app.html.heex create mode 100644 apps/object_storage/lib/object_storage_web/templates/layout/live.html.heex create mode 100644 apps/object_storage/lib/object_storage_web/templates/layout/root.html.heex create mode 100644 apps/object_storage/lib/object_storage_web/templates/page/index.html.heex create mode 100644 apps/object_storage/lib/object_storage_web/views/changeset_view.ex create mode 100644 apps/object_storage/lib/object_storage_web/views/error_helpers.ex create mode 100644 apps/object_storage/lib/object_storage_web/views/error_view.ex create mode 100644 apps/object_storage/lib/object_storage_web/views/layout_view.ex create mode 100644 apps/object_storage/lib/object_storage_web/views/page_view.ex create mode 100644 apps/object_storage/lib/object_storage_web/views/upload_view.ex create mode 100644 apps/object_storage/mix.exs create mode 100644 apps/object_storage/priv/repo/migrations/.formatter.exs create mode 100644 apps/object_storage/priv/repo/migrations/20210928031038_create_storage_objects.exs create mode 100644 apps/object_storage/priv/repo/migrations/20211005220041_create_storage_object_chunk.exs create mode 100644 apps/object_storage/priv/repo/seeds.exs create mode 100644 apps/object_storage/test.dmg create mode 100644 apps/object_storage/test/object_storage/object_chunk_test.exs create mode 100644 apps/object_storage/test/object_storage/object_test.exs create mode 100644 apps/object_storage/test/object_storage/objects_test.exs create mode 100644 apps/object_storage/test/object_storage_test.exs create mode 100644 apps/object_storage/test/object_storage_web/controllers/chunked_upload_controller_test.exs create mode 100644 apps/object_storage/test/object_storage_web/controllers/upload_controller_test.exs create mode 100644 apps/object_storage/test/object_storage_web/plugs/check_signatures/signature_generator_test.exs create mode 100644 apps/object_storage/test/object_storage_web/plugs/check_signatures_test.exs create mode 100644 apps/object_storage/test/object_storage_web/views/error_view_test.exs create mode 100644 apps/object_storage/test/object_storage_web/views/layout_view_test.exs create mode 100644 apps/object_storage/test/object_storage_web/views/page_view_test.exs create mode 100644 apps/object_storage/test/seed_sets/.keep create mode 100644 apps/object_storage/test/seed_sets/test_end_to_end_test.exs create mode 100644 apps/object_storage/test/support/channel_case.ex create mode 100644 apps/object_storage/test/support/conn_case.ex create mode 100644 apps/object_storage/test/support/data_case.ex create mode 100644 apps/object_storage/test/support/signature_testing_utilities.ex create mode 100644 apps/object_storage/test/test_helper.exs 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 0000000000000000000000000000000000000000..9f3c8fc84aa1cc24a8378cfd8be30841cf4688da GIT binary patch literal 127935 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

^ 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 EKLfT8p#T5? literal 0 HcmV?d00001 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"},