From 20ddad5636aafcb516fb0258bdda1a51543a45b4 Mon Sep 17 00:00:00 2001 From: Roy Schut Date: Mon, 16 Sep 2024 14:02:58 +0200 Subject: [PATCH 01/16] chore: maintenance 2024 q3 (#608) * chore(project): remove obsolete dep resolutions * chore(project): add resolutions for high risk dep issues * chore(project): add resolution for medium risk dep issues * chore(project): update date-fns * chore(project): update dompurify * chore(project): update fast-xml-parser * chore(project): update zustand * chore(project): perform yarn upgrade for payment deps * chore(project): perform yarn upgrade (interactive) * chore(project): sync yarn lock * refactor(project): fix incorrect store update * refactor(project): update yarn lock * refactor(project): remove deprecated zustand State type * chore(project): update syncpack rules for non-peer local packages --- .syncpackrc.json | 82 +-- configs/eslint-config-jwp/package.json | 2 +- package.json | 14 +- packages/common/package.json | 6 +- .../common/src/controllers/AppController.ts | 6 +- packages/common/src/stores/utils.ts | 6 +- packages/common/src/utils/compare.ts | 2 +- packages/hooks-react/package.json | 2 +- packages/ui-react/package.json | 12 +- platforms/web/package.json | 8 +- yarn.lock | 662 +++++++----------- 11 files changed, 300 insertions(+), 502 deletions(-) diff --git a/.syncpackrc.json b/.syncpackrc.json index 5de916305..118467bd6 100644 --- a/.syncpackrc.json +++ b/.syncpackrc.json @@ -1,88 +1,38 @@ { "$schema": "https://unpkg.com/syncpack@11.2.1/dist/schema.json", - "sortFirst": [ - "name", - "description", - "version", - "private", - "license", - "repository", - "author", - "main", - "exports", - "engines", - "workspaces", - "scripts" - ], + "sortFirst": ["name", "description", "version", "private", "license", "repository", "author", "main", "exports", "engines", "workspaces", "scripts"], "semverGroups": [ { - "dependencies": [ - "codeceptjs", - "codeceptjs**", - "react-router", - "react-router-dom", - "typescript" - ], - "packages": [ - "**" - ], + "dependencies": ["codeceptjs", "codeceptjs**", "react-router", "react-router-dom", "typescript"], + "packages": ["**"], "isIgnored": true }, { "range": "^", - "dependencies": [ - "**" - ], - "packages": [ - "**" - ], - "dependencyTypes": [ - "prod", - "dev", - "peer" - ] + "dependencies": ["**"], + "packages": ["**"], + "dependencyTypes": ["prod", "dev", "peer"] } ], "versionGroups": [ { "label": "Ensure semver ranges for locally developed packages satisfy the local version", - "dependencies": [ - "@jwp/**", - "**-config-jwp" - ], - "dependencyTypes": [ - "peer" - ], - "packages": [ - "**" - ], + "dependencies": ["@jwp/**", "**-config-jwp"], + "dependencyTypes": ["dev", "prod", "peer"], + "packages": ["**"], "pinVersion": "*" }, { - "label": "Ensure local packages are installed as peerDependency", - "dependencies": [ - "@jwp/**", - "**-config-jwp" - ], - "dependencyTypes": [ - "dev", - "prod" - ], - "packages": [ - "**" - ], + "label": "Ensure local packages are installed as dev or prod dependency", + "dependencies": ["@jwp/**", "**-config-jwp"], + "dependencyTypes": ["peer"], + "packages": ["**"], "isBanned": true }, { - "dependencies": [ - "@types/**" - ], - "dependencyTypes": [ - "!dev" - ], - "packages": [ - "**" - ], + "dependencies": ["@types/**"], + "dependencyTypes": ["!dev"], + "packages": ["**"], "isBanned": true, "label": "@types packages should only be under devDependencies" } diff --git a/configs/eslint-config-jwp/package.json b/configs/eslint-config-jwp/package.json index 557909bb0..6ae6258e3 100644 --- a/configs/eslint-config-jwp/package.json +++ b/configs/eslint-config-jwp/package.json @@ -11,7 +11,7 @@ "@typescript-eslint/parser": "^7.13.1", "confusing-browser-globals": "^1.0.11", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react": "^7.34.2", + "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2" } } diff --git a/package.json b/package.json index 3d0ce78c5..c92bd4ca6 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ "devDependencies": { "@commitlint/cli": "^17.8.1", "@commitlint/config-conventional": "^17.8.1", - "@types/node": "^18.19.36", + "@types/node": "^18.19.37", "csv-parse": "^5.5.6", "eslint": "^8.57.0", "husky": "^6.0.0", "i18next-parser-workspaces": "^0.2.0", - "knip": "^5.21.1", - "lint-staged": "^15.2.7", + "knip": "^5.30.1", + "lint-staged": "^15.2.10", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "read": "^2.1.0", @@ -56,10 +56,8 @@ "eslint-config-jwp": "*" }, "resolutions": { - "codeceptjs/**/ansi-regex": "^4.1.1", - "codeceptjs/**/minimatch": "^3.0.5", - "flat": "^5.0.1", - "glob-parent": "^5.1.2", - "json5": "^2.2.2" + "codeceptjs/**/fast-xml-parser": "^4.5.0", + "micromatch": ">=4.0.8", + "ws": ">=5.2.4" } } diff --git a/packages/common/package.json b/packages/common/package.json index d93c32b25..784d8029e 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -11,8 +11,8 @@ "dependencies": { "@inplayer-org/inplayer.js": "^3.13.28", "broadcast-channel": "^7.0.0", - "date-fns": "^2.30.0", - "fast-xml-parser": "^4.4.0", + "date-fns": "^3.6.0", + "fast-xml-parser": "^4.5.0", "i18next": "^22.5.1", "ini": "^3.0.1", "inversify": "^6.0.2", @@ -21,7 +21,7 @@ "react-i18next": "^12.3.1", "reflect-metadata": "^0.2.2", "yup": "^0.32.11", - "zustand": "^3.7.2" + "zustand": "^4.5.5" }, "devDependencies": { "@types/ini": "^1.3.34", diff --git a/packages/common/src/controllers/AppController.ts b/packages/common/src/controllers/AppController.ts index e750f5c67..6d86d6d36 100644 --- a/packages/common/src/controllers/AppController.ts +++ b/packages/common/src/controllers/AppController.ts @@ -51,11 +51,7 @@ export default class AppController { } // Store the logo right away and set css variables so the error page will be branded - const banner = config.assets.banner; - - useConfigStore.setState((s) => { - s.config.assets.banner = banner; - }); + useConfigStore.setState((state) => merge({}, state, { config: { assets: { banner: config.assets.banner } } })); config = await this.configService.validateConfig(config); config = merge({}, defaultConfig, config); diff --git a/packages/common/src/stores/utils.ts b/packages/common/src/stores/utils.ts index 63b1b9dca..cf664f60b 100644 --- a/packages/common/src/stores/utils.ts +++ b/packages/common/src/stores/utils.ts @@ -1,10 +1,10 @@ -import type { State, StateCreator } from 'zustand'; -import create from 'zustand'; +import type { StateCreator } from 'zustand'; +import { create } from 'zustand'; import { devtools, subscribeWithSelector } from 'zustand/middleware'; import { IS_DEVELOPMENT_BUILD, IS_TEST_MODE } from '../utils/common'; -export const createStore = (name: string, storeFn: StateCreator) => { +export const createStore = (name: string, storeFn: StateCreator) => { const store = subscribeWithSelector(storeFn); // https://github.com/pmndrs/zustand/issues/852#issuecomment-1059783350 diff --git a/packages/common/src/utils/compare.ts b/packages/common/src/utils/compare.ts index 84782b654..c08793f8a 100644 --- a/packages/common/src/utils/compare.ts +++ b/packages/common/src/utils/compare.ts @@ -1,3 +1,3 @@ -import shallow from 'zustand/shallow'; +import { shallow } from 'zustand/shallow'; export { shallow }; diff --git a/packages/hooks-react/package.json b/packages/hooks-react/package.json index f6654f020..ff6d7af68 100644 --- a/packages/hooks-react/package.json +++ b/packages/hooks-react/package.json @@ -9,7 +9,7 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "date-fns": "^2.30.0", + "date-fns": "^3.6.0", "i18next": "^22.5.1", "planby": "^0.3.0", "react": "^18.3.1", diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index 547c4eda2..28d1e1c42 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -9,15 +9,15 @@ "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest" }, "dependencies": { - "@adyen/adyen-web": "^5.66.1", + "@adyen/adyen-web": "^5.68.1", "@videodock/tile-slider": "^2.0.0", "classnames": "^2.5.1", - "date-fns": "^2.30.0", - "dompurify": "^2.5.5", + "date-fns": "^3.6.0", + "dompurify": "^3.1.6", "i18next": "^22.5.1", "inversify": "^6.0.2", "marked": "^4.3.0", - "payment": "^2.4.6", + "payment": "^2.4.7", "planby": "^0.3.0", "react": "^18.3.1", "react-app-polyfill": "^3.0.0", @@ -34,7 +34,7 @@ "devDependencies": { "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^14.3.1", - "@types/dompurify": "^2.4.0", + "@types/dompurify": "^3.0.5", "@types/jwplayer": "^8.31.1", "@types/marked": "^4.3.2", "@types/payment": "^2.1.7", @@ -47,7 +47,7 @@ "vite-plugin-svgr": "^4.2.0", "vitest": "^1.6.0", "vitest-axe": "^1.0.0-pre.3", - "wicg-inert": "^3.1.2" + "wicg-inert": "^3.1.3" }, "peerDependencies": { "@jwp/ott-common": "*", diff --git a/platforms/web/package.json b/platforms/web/package.json index 4582db208..b78345434 100644 --- a/platforms/web/package.json +++ b/platforms/web/package.json @@ -34,14 +34,14 @@ "@jwp/ott-ui-react": "*", "i18next": "^22.5.1", "i18next-browser-languagedetector": "^6.1.8", - "i18next-http-backend": "^2.5.2", + "i18next-http-backend": "^2.6.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", "react-i18next": "^12.3.1", "react-router": "6.14.2", "react-router-dom": "6.14.2", - "wicg-inert": "^3.1.2" + "wicg-inert": "^3.1.3" }, "devDependencies": { "@babel/core": "^7.24.7", @@ -50,7 +50,7 @@ "@testing-library/jest-dom": "^6.4.6", "@types/jwplayer": "^8.31.1", "@types/luxon": "^3.4.2", - "@types/node": "^18.19.36", + "@types/node": "^18.19.37", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-helmet": "^6.1.11", @@ -66,7 +66,7 @@ "i18next-parser": "^8.13.0", "jsdom": "^22.1.0", "luxon": "^3.4.4", - "playwright": "^1.44.5", + "playwright": "^1.45.0", "postcss": "^8.4.38", "postcss-config-jwp": "*", "react-app-polyfill": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 66478c708..2671a7a1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,15 +12,15 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== -"@adyen/adyen-web@^5.66.1": - version "5.66.1" - resolved "https://registry.yarnpkg.com/@adyen/adyen-web/-/adyen-web-5.66.1.tgz#0da141f138487e4901f61ee6b9761281f0f91fd4" - integrity sha512-VAXK4hZrK5YyHQC/fLqBe/iArwmJj8KMfLD67s6rLPpsBKvKFGglJNB7CkoBUjN0KLCQ/1rSx+IlBnC33sJIxw== +"@adyen/adyen-web@^5.68.1": + version "5.68.1" + resolved "https://registry.yarnpkg.com/@adyen/adyen-web/-/adyen-web-5.68.1.tgz#f19a30f524fdbb1f0252b5ea77c6a12c14af630a" + integrity sha512-+dHmXOJVKPXrHi9Afq02Hqdnis+e2abQSVRkg5HmS17UqiJ9zTLlQdbEkDCakana4BH7M1ZrwfbUkFUgO0NL5w== dependencies: "@babel/runtime" "^7.15.4" "@babel/runtime-corejs3" "^7.20.1" "@types/applepayjs" "14.0.6" - "@types/googlepay" "^0.7.0" + "@types/googlepay" "0.7.6" classnames "^2.3.1" core-js-pure "^3.25.3" preact "10.13.2" @@ -1167,9 +1167,9 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs3@^7.20.1": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz#65a99097e4c28e6c3a174825591700cc5abd710e" - integrity sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA== + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.25.6.tgz#5e3facf42775cc95bcde95746e940061931286e4" + integrity sha512-Gz0Nrobx8szge6kQQ5Z5MX9L3ObqNwCQY1PSwSNzreFL7aHGxv8Fp2j3ETV6/wWdbiV+mW6OSm8oQhg3Tcsniw== dependencies: core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" @@ -1181,13 +1181,20 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.0", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.15.4": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" + integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.0.0", "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -1606,29 +1613,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== -"@ericcornelissen/bash-parser@0.5.3": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@ericcornelissen/bash-parser/-/bash-parser-0.5.3.tgz#cda9f0e9ed3bcf62c29c277de778726425e03b0a" - integrity sha512-9Z0sGuXqf6En19qmwB0Syi1Mc8TYl756dNuuaYal9mrypKa0Jq/IX6aJfh6Rk2S3z66KBisWTqloDo7weYj4zg== - dependencies: - array-last "^1.1.1" - babylon "^6.9.1" - compose-function "^3.0.3" - filter-obj "^1.1.0" - has-own-property "^0.1.0" - identity-function "^1.0.0" - is-iterable "^1.1.0" - iterable-lookahead "^1.0.0" - lodash.curry "^4.1.1" - magic-string "^0.16.0" - map-obj "^2.0.0" - object-pairs "^0.1.0" - object-values "^1.0.0" - reverse-arguments "^1.0.0" - shell-quote-word "^1.0.1" - to-pascal-case "^1.0.0" - unescape-js "^1.0.5" - "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -2240,33 +2224,12 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.scandir@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-3.0.0.tgz#91c0a33e1aeaedcd4bab2bf31be5d1962a55d2a7" - integrity sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg== - dependencies: - "@nodelib/fs.stat" "3.0.0" - run-parallel "^1.2.0" - "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.stat@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-3.0.0.tgz#ef6c829f2b05f42595d88854ebd777d4335ff0a9" - integrity sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ== - -"@nodelib/fs.walk@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-2.0.0.tgz#10499ac2210f6399770b465ba728adafc7d44bb1" - integrity sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A== - dependencies: - "@nodelib/fs.scandir" "3.0.0" - fastq "^1.15.0" - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@1.2.8", "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -3103,10 +3066,10 @@ dependencies: "@babel/types" "^7.20.7" -"@types/dompurify@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" - integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== dependencies: "@types/trusted-types" "*" @@ -3128,7 +3091,7 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/googlepay@^0.7.0": +"@types/googlepay@0.7.6": version "0.7.6" resolved "https://registry.yarnpkg.com/@types/googlepay/-/googlepay-0.7.6.tgz#ba444ad8b2945e70f873673b8f5371745b8cfe37" integrity sha512-5003wG+qvf4Ktf1hC9IJuRakNzQov00+Xf09pAWGJLpdOjUrq0SSLCpXX7pwSeTG9r5hrdzq1iFyZcW7WVyr4g== @@ -3216,10 +3179,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== -"@types/node@^18.19.36": - version "18.19.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.37.tgz#506ee89d6b5edd5a4c4e01b22162dd8309718a35" - integrity sha512-Pi53fdVMk7Ig5IfAMltQQMgtY7xLzHaEous8IQasYsdQbYK3v90FkxI3XYQCe/Qme58pqp14lXJIsFmGP8VoZQ== +"@types/node@^18.19.37": + version "18.19.50" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.50.tgz#8652b34ee7c0e7e2004b3f08192281808d41bf5a" + integrity sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg== dependencies: undici-types "~5.26.4" @@ -3734,10 +3697,12 @@ ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" - integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" ansi-fragments@^0.2.1: version "0.2.1" @@ -3753,7 +3718,7 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== -ansi-regex@^4.1.0, ansi-regex@^4.1.1: +ansi-regex@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== @@ -3764,9 +3729,9 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" @@ -3836,11 +3801,6 @@ aria-query@^5.0.0: dependencies: dequal "^2.0.3" -arity-n@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" - integrity sha512-fExL2kFDC1Q2DUOx3whE/9KoN66IzkY4b4zUHUBFM1ojEYjZZYDcUW3bek/ufGionX9giIKDC5redH2IlGqcQQ== - array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" @@ -3866,13 +3826,6 @@ array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8: get-intrinsic "^1.2.4" is-string "^1.0.7" -array-last@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336" - integrity sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg== - dependencies: - is-number "^4.0.0" - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -3934,16 +3887,6 @@ array.prototype.flatmap@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.toreversed@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" - integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - array.prototype.tosorted@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" @@ -4018,11 +3961,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async@^3.2.3, async@^3.2.4: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" @@ -4133,11 +4071,6 @@ babel-plugin-transform-typescript-metadata@^0.3.2: dependencies: "@babel/helper-plugin-utils" "^7.0.0" -babylon@^6.9.1: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -4791,12 +4724,12 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" - integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== dependencies: - restore-cursor "^4.0.0" + restore-cursor "^5.0.0" cli-spinners@^2.5.0: version "2.9.2" @@ -5054,13 +4987,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compose-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" - integrity sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg== - dependencies: - arity-n "^1.0.4" - compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -5210,9 +5136,9 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1: browserslist "^4.23.0" core-js-pure@^3.25.3, core-js-pure@^3.30.2: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" - integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== + version "3.38.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.38.1.tgz#e8534062a54b7221344884ba9b52474be495ada3" + integrity sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ== core-js@^3.19.2, core-js@^3.37.1: version "3.37.1" @@ -5460,13 +5386,18 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -date-fns@^2.28.0, date-fns@^2.30.0: +date-fns@^2.28.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== dependencies: "@babel/runtime" "^7.21.0" +date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + dayjs@^1.8.15: version "1.11.11" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" @@ -5484,7 +5415,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.5, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.4: +debug@4, debug@4.3.5, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -5505,6 +5436,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@~4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -5654,7 +5592,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -5887,10 +5825,10 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^2.5.5: - version "2.5.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.5.tgz#0540a05b8020d4691ee9c6083fb23b2c919276fc" - integrity sha512-FgbqnEPiv5Vdtwt6Mxl7XSylttCC03cqP5ldNT2z+Kj0nLxPHJH4+1Cyf5Jasxhw93Rl4Oo11qRoUV72fmya2Q== +dompurify@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2" + integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ== domutils@^2.8.0: version "2.8.0" @@ -6046,9 +5984,9 @@ email-addresses@^5.0.0: integrity sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw== emoji-regex@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" - integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== emoji-regex@^8.0.0: version "8.0.0" @@ -6072,6 +6010,14 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enquirer@^2.3.6: version "2.4.1" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" @@ -6105,6 +6051,11 @@ envinfo@^7.10.0: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + eol@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" @@ -6144,7 +6095,7 @@ errorhandler@^1.5.1: accepts "~1.3.7" escape-html "~1.0.3" -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -6420,29 +6371,29 @@ eslint-plugin-react-hooks@^4.6.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -eslint-plugin-react@^7.34.2: - version "7.34.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" - integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA== +eslint-plugin-react@^7.34.3: + version "7.35.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz#d32500d3ec268656d5071918bfec78cfd8b070ed" + integrity sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" - array.prototype.toreversed "^1.1.2" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.8" object.fromentries "^2.0.8" - object.hasown "^1.1.4" object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-scope@^7.2.2: version "7.2.2" @@ -6677,10 +6628,10 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-xml-parser@^4.0.12, fast-xml-parser@^4.2.4, fast-xml-parser@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501" - integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg== +fast-xml-parser@^4.0.12, fast-xml-parser@^4.2.4, fast-xml-parser@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37" + integrity sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg== dependencies: strnum "^1.0.5" @@ -6689,7 +6640,7 @@ fastest-levenshtein@^1.0.16: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== -fastq@^1.13.0, fastq@^1.15.0, fastq@^1.6.0: +fastq@^1.13.0, fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== @@ -6862,7 +6813,7 @@ flat-cache@^3.0.4, flat-cache@^3.2.0: keyv "^4.5.3" rimraf "^3.0.2" -flat@^5.0.1, flat@^5.0.2: +flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== @@ -7183,13 +7134,20 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@^6.0.2, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-stream@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-8.0.2.tgz#09e5818e41c16dd85274d72c7a7158d307426313" @@ -7296,7 +7254,7 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.2, globalthis@^1.0.3: +globalthis@^1.0.3, globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -7376,11 +7334,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-own-property@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/has-own-property/-/has-own-property-0.1.0.tgz#992b0f5bb3a25416f8d4d0cde53f497b9d7b1ea5" - integrity sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw== - has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -7629,10 +7582,10 @@ i18next-browser-languagedetector@^6.1.8: dependencies: "@babel/runtime" "^7.19.0" -i18next-http-backend@^2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz#3d846cc239987fe7700d1cf0f17975807bfd25d3" - integrity sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw== +i18next-http-backend@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz#186c3a1359e10245c9119a13129f9b5bf328c9a7" + integrity sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog== dependencies: cross-fetch "4.0.0" @@ -7714,17 +7667,17 @@ idb@^7.0.1: resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== -identity-function@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/identity-function/-/identity-function-1.0.0.tgz#bea1159f0985239be3ca348edf40ce2f0dd2c21d" - integrity sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw== - ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.1.8, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: +ignore@^5.1.8: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== @@ -7940,7 +7893,14 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0: +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-core-module@^2.13.1, is-core-module@^2.5.0: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== @@ -8024,11 +7984,6 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== -is-iterable@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-iterable/-/is-iterable-1.1.1.tgz#71f9aa6f113e1d968ebe1d41cff4c8fb23a817bc" - integrity sha512-EdOZCr0NsGE00Pot+x1ZFx9MJK3C6wy91geZpXwvwexDLJvA4nzYyZf7r+EIwSeVsOLDdBz7ATg9NqKTzuNYuQ== - is-map@^2.0.2, is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" @@ -8056,11 +8011,6 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -8284,11 +8234,6 @@ istanbul-reports@^3.1.5, istanbul-reports@^3.1.7: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterable-lookahead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/iterable-lookahead/-/iterable-lookahead-1.0.0.tgz#896dfcb78680bdb50036e97edb034c8b68a9737f" - integrity sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ== - iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -8417,7 +8362,7 @@ jest-worker@^29.6.3: merge-stream "^2.0.0" supports-color "^8.0.0" -jiti@^1.21.0: +jiti@^1.21.0, jiti@^1.21.6: version "1.21.6" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== @@ -8643,7 +8588,14 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^1.0.2, json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -8716,27 +8668,25 @@ klona@^2.0.4, klona@^2.0.6: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== -knip@^5.21.1: - version "5.22.0" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.22.0.tgz#e91620d833ed9331efc3e1dc079c5d8ab5c12f21" - integrity sha512-ijGbuB/622oL/rhg5kPR7U26xebu7czEu50RAxjh66YXkpvXW67Dv9Oz48yBSbGFMEWylX9aOe+NeHJbVe68jw== +knip@^5.30.1: + version "5.30.1" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.30.1.tgz#cfd4dca122b2bf1920fd99f2b49cc6ded60eff91" + integrity sha512-20XqtThAIuNNbEJjAdDTIUjSTx89bez5MukykKvaZEvLYLXOmtaXsSVPU6WQuFZ51/MolXctQllqHNzHxS210w== dependencies: - "@ericcornelissen/bash-parser" "0.5.3" - "@nodelib/fs.walk" "2.0.0" + "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" easy-table "1.2.0" + enhanced-resolve "^5.17.1" fast-glob "^3.3.2" - jiti "^1.21.0" + jiti "^1.21.6" js-yaml "^4.1.0" minimist "^1.2.8" picocolors "^1.0.0" picomatch "^4.0.1" pretty-ms "^9.0.0" - resolve "^1.22.8" smol-toml "^1.1.4" strip-json-comments "5.0.1" summary "2.1.0" - tsconfig-paths "^4.2.0" zod "^3.22.4" zod-validation-error "^3.0.3" @@ -8850,7 +8800,7 @@ lilconfig@^2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== -lilconfig@^3.0.0, lilconfig@~3.1.1: +lilconfig@^3.0.0, lilconfig@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== @@ -8860,32 +8810,32 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lint-staged@^15.2.7: - version "15.2.7" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" - integrity sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw== +lint-staged@^15.2.10: + version "15.2.10" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.10.tgz#92ac222f802ba911897dcf23671da5bb80643cd2" + integrity sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg== dependencies: chalk "~5.3.0" commander "~12.1.0" - debug "~4.3.4" + debug "~4.3.6" execa "~8.0.1" - lilconfig "~3.1.1" - listr2 "~8.2.1" - micromatch "~4.0.7" + lilconfig "~3.1.2" + listr2 "~8.2.4" + micromatch "~4.0.8" pidtree "~0.6.0" string-argv "~0.3.2" - yaml "~2.4.2" + yaml "~2.5.0" -listr2@~8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.1.tgz#06a1a6efe85f23c5324180d7c1ddbd96b5eefd6d" - integrity sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g== +listr2@~8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f" + integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g== dependencies: cli-truncate "^4.0.0" colorette "^2.0.20" eventemitter3 "^5.0.1" - log-update "^6.0.0" - rfdc "^1.3.1" + log-update "^6.1.0" + rfdc "^1.4.1" wrap-ansi "^9.0.0" load-json-file@^4.0.0: @@ -8976,11 +8926,6 @@ lodash.clonedeep@4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== -lodash.curry@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" - integrity sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA== - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -9059,14 +9004,14 @@ log-symbols@4.1.0, log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log-update@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.0.0.tgz#0ddeb7ac6ad658c944c1de902993fce7c33f5e59" - integrity sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw== +log-update@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.1.0.tgz#1a04ff38166f94647ae1af562f4bd6a15b1b7cd4" + integrity sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w== dependencies: - ansi-escapes "^6.2.0" - cli-cursor "^4.0.0" - slice-ansi "^7.0.0" + ansi-escapes "^7.0.0" + cli-cursor "^5.0.0" + slice-ansi "^7.1.0" strip-ansi "^7.1.0" wrap-ansi "^9.0.0" @@ -9174,13 +9119,6 @@ lz-utils@^2.0.2: resolved "https://registry.yarnpkg.com/lz-utils/-/lz-utils-2.0.2.tgz#9ccf1f76400617da5b3f5a05192f5227cffd6881" integrity sha512-i1PJN4hNEevkrvLMqNWCCac1BcB5SRaghywG7HVzWOyVkFOasLCG19ND1sY1F/ZEsM6SnGtoXyBWnmfqOM5r6g== -magic-string@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.16.0.tgz#970ebb0da7193301285fb1aa650f39bdd81eb45a" - integrity sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ== - dependencies: - vlq "^0.2.1" - magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -9234,11 +9172,6 @@ map-obj@^1.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ== - map-obj@^4.0.0, map-obj@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" @@ -9531,10 +9464,10 @@ metro@0.80.9, metro@^0.80.3: ws "^7.5.1" yargs "^17.6.2" -micromatch@^4.0.4, micromatch@^4.0.5, micromatch@~4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== +micromatch@>=4.0.8, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@~4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" @@ -9586,6 +9519,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -9798,7 +9736,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -10108,16 +10046,6 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-pairs@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-pairs/-/object-pairs-0.1.0.tgz#8276eed81d60b8549d69c5f73a682ab9da4ff32f" - integrity sha512-3ECr6K831I4xX/Mduxr9UC+HPOz/d6WKKYj9p4cmC8Lg8p7g8gitzsxNX5IWlSIgFWN/a4JgrJaoAMKn20oKwA== - -object-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/object-values/-/object-values-1.0.0.tgz#72af839630119e5b98c3b02bb8c27e3237158105" - integrity sha512-+8hwcz/JnQ9EpLIXzN0Rs7DLsBpJNT/xYehtB/jU93tHYr5BFEO8E+JGQNOSqE7opVzz5cGksKFHt7uUJVLSjQ== - object.assign@^4.1.4, object.assign@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" @@ -10156,15 +10084,6 @@ object.groupby@^1.0.1: define-properties "^1.2.1" es-abstract "^1.23.2" -object.hasown@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" - integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== - dependencies: - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" @@ -10231,6 +10150,13 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + open@^6.2.0: version "6.4.0" resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" @@ -10601,12 +10527,12 @@ pathval@^2.0.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== -payment@^2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/payment/-/payment-2.4.6.tgz#a69bfae5ee6edb2210d8c9f7720ee702359a40f5" - integrity sha512-QSCAa1yQSkqbe4Ghac3sSA5SQ+Cxc3e4xwCxxun5NT6hUSWsNXXlN8KCCY0kAFFXBP9C7DrfyXP4REB7nPJa8g== +payment@^2.4.7: + version "2.4.7" + resolved "https://registry.yarnpkg.com/payment/-/payment-2.4.7.tgz#fabefae499050f159e2a6837a58d30b78789f91c" + integrity sha512-5HyD3HJ0q3XtVm/Dss4t3KGtIughkCfnyQ1WfMashVD2vvBOmuXnkswhBcknMcHW8oo4zqVnNbQSkNaqafYaGA== dependencies: - globalthis "^1.0.2" + globalthis "^1.0.4" qj "~2.0.0" pend@~1.2.0: @@ -10626,7 +10552,12 @@ phin@^3.7.0: dependencies: centra "^2.7.0" -picocolors@^1.0.0, picocolors@^1.0.1: +picocolors@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + +picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== @@ -10716,17 +10647,17 @@ planby@^0.3.0: date-fns "^2.28.0" use-debounce "^7.0.1" -playwright-core@1.45.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.0.tgz#5741a670b7c9060ce06852c0051d84736fb94edc" - integrity sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ== +playwright-core@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.0.tgz#b54ec060fd83e5c2e46b63986b5ebb5e96ace427" + integrity sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg== -playwright@^1.44.5: - version "1.45.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.0.tgz#400c709c64438690f13705cb9c88ef93089c5c27" - integrity sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA== +playwright@^1.45.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.0.tgz#fb9b028883fad11362f9ff63ce7ba44bda0bf626" + integrity sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww== dependencies: - playwright-core "1.45.0" + playwright-core "1.47.0" optionalDependencies: fsevents "2.3.2" @@ -10905,9 +10836,9 @@ pretty-format@^29.7.0: react-is "^18.0.0" pretty-ms@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.0.0.tgz#53c57f81171c53be7ce3fd20bdd4265422bc5929" - integrity sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng== + version "9.1.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.1.0.tgz#0ad44de6086454f48a168e5abb3c26f8db1b3253" + integrity sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw== dependencies: parse-ms "^4.0.0" @@ -11627,7 +11558,7 @@ resolve-options@^2.0.0: dependencies: value-or-function "^4.0.0" -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22.8: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -11668,13 +11599,13 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -restore-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" - integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" + onetime "^7.0.0" + signal-exit "^4.1.0" retry@^0.10.0: version "0.10.1" @@ -11691,12 +11622,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -reverse-arguments@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/reverse-arguments/-/reverse-arguments-1.0.0.tgz#c28095a3a921ac715d61834ddece9027992667cd" - integrity sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ== - -rfdc@^1.3.1: +rfdc@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== @@ -11796,7 +11722,7 @@ run-async@^2.2.0: resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -run-parallel@^1.1.9, run-parallel@^1.2.0: +run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== @@ -12107,11 +12033,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote-word@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/shell-quote-word/-/shell-quote-word-1.0.1.tgz#e2bdfd22d599fd68886491677e38f560f9d469c9" - integrity sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg== - shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" @@ -12199,7 +12120,7 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -slice-ansi@^7.0.0: +slice-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== @@ -12218,9 +12139,9 @@ smob@^1.0.0: integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== smol-toml@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.2.1.tgz#6216334548763d4aac76cafff19f8914937ee13a" - integrity sha512-OtZKrVrGIT+m++lxyF0z5n68nkwdgZotPhy89bfA4T7nSWe0xeQtfbjM1z5VLTilJdWXH46g8i0oAcpQNkzZTg== + version "1.3.0" + resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.0.tgz#5200e251fffadbb72570c84e9776d2a3eca48143" + integrity sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA== snake-case@^3.0.4: version "3.0.4" @@ -12457,7 +12378,7 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12474,15 +12395,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -12493,19 +12405,14 @@ string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" string-width@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.1.0.tgz#d994252935224729ea3719c49f7206dc9c46550a" - integrity sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw== + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== dependencies: emoji-regex "^10.3.0" get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.fromcodepoint@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653" - integrity sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg== - string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" @@ -12534,6 +12441,14 @@ string.prototype.padend@^3.0.0: es-abstract "^1.23.2" es-object-atoms "^1.0.0" +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -12585,7 +12500,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12606,13 +12521,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -12868,6 +12776,11 @@ table@^6.8.1: string-width "^4.2.3" strip-ansi "^6.0.1" +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + tar-fs@2.1.1, tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -13103,18 +13016,6 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== -to-no-case@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" - integrity sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg== - -to-pascal-case@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-pascal-case/-/to-pascal-case-1.0.0.tgz#0bbdc8df448886ba01535e543327048d0aa1ce78" - integrity sha512-QGMWHqM6xPrcQW57S23c5/3BbYb0Tbe9p+ur98ckRnGDwD4wbbtDiYI38CfmMKNB5Iv0REjs5SNDntTwvDxzZA== - dependencies: - to-space-case "^1.0.0" - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -13122,13 +13023,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -to-space-case@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17" - integrity sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA== - dependencies: - to-no-case "^1.0.0" - to-through@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/to-through/-/to-through-3.0.0.tgz#bf4956eaca5a0476474850a53672bed6906ace54" @@ -13438,11 +13332,6 @@ ufo@^1.5.3: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -13483,13 +13372,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -unescape-js@^1.0.5: - version "1.1.4" - resolved "https://registry.yarnpkg.com/unescape-js/-/unescape-js-1.1.4.tgz#4bc6389c499cb055a98364a0b3094e1c3d5da395" - integrity sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g== - dependencies: - string.fromcodepoint "^0.2.1" - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -13591,6 +13473,11 @@ use-debounce@^7.0.1: resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-7.0.1.tgz#380e6191cc13ad29f8e2149a12b5c37cc2891190" integrity sha512-fOrzIw2wstbAJuv8PC9Vg4XgwyTLEOdq4y/Z3IhVl8DAE4svRcgyEUvrEXu+BMNgMoc3YND6qLT61kkgEKXh7Q== +use-sync-external-store@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + userhome@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/userhome/-/userhome-1.0.0.tgz#b6491ff12d21a5e72671df9ccc8717e1c6688c0b" @@ -13840,11 +13727,6 @@ vitest@^1.6.0: vite-node "1.6.0" why-is-node-running "^2.2.2" -vlq@^0.2.1: - version "0.2.3" - resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" - integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== - vlq@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" @@ -14061,10 +13943,10 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" -wicg-inert@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.2.tgz#df10cf756b773a96fce107c3ddcd43be5d1e3944" - integrity sha512-Ba9tGNYxXwaqKEi9sJJvPMKuo063umUPsHN0JJsjrs2j8KDSzkWLMZGZ+MH1Jf1Fq4OWZ5HsESJID6nRza2ang== +wicg-inert@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.3.tgz#e53dbc9ac1e0d7f8c60f25e707614a835986272a" + integrity sha512-5L0PKK7iP+0Q/jv2ccgmkz/pfXbumZtlEyWS/xnX+L+Og3f7WjL4+iEs18k4IuldOX3PgGpza3qGndL9xUBjCQ== word-wrap@^1.2.5: version "1.2.5" @@ -14392,7 +14274,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14410,15 +14292,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -14469,36 +14342,10 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - -ws@8.17.1, ws@^8.13.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -ws@^3.2.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" - integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== - dependencies: - async-limiter "~1.0.0" - safe-buffer "~5.1.0" - ultron "~1.1.0" - -ws@^6.2.2: - version "6.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" - integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== - dependencies: - async-limiter "~1.0.0" - -ws@^7, ws@^7.0.0, ws@^7.5.0, ws@^7.5.1: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@8.13.0, ws@8.17.1, ws@>=5.2.4, ws@^3.2.0, ws@^6.2.2, ws@^7, ws@^7.0.0, ws@^7.5.0, ws@^7.5.1, ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xdg-basedir@^4.0.0: version "4.0.0" @@ -14560,11 +14407,16 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.2.1, yaml@~2.4.2: +yaml@^2.2.1: version "2.4.5" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== +yaml@~2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" + integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" @@ -14691,16 +14543,18 @@ yup@^0.32.11: toposort "^2.0.2" zod-validation-error@^3.0.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.0.tgz#2cfe81b62d044e0453d1aa3ae7c32a2f36dde9af" - integrity sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw== + version "3.3.1" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.1.tgz#86adc781129d1a7fed3c3e567e8dbe7c4a15eaa4" + integrity sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA== zod@3.23.8, zod@^3.22.4: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== -zustand@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d" - integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA== +zustand@^4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" + integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== + dependencies: + use-sync-external-store "1.2.2" From 80acd7f6c6672baf6e4a01dbecdf8540be68dbd7 Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:46:45 +0300 Subject: [PATCH 02/16] feat(menu): support media type for menu (#610) * feat(menu): support media type for menu * feat(menu): fix lint fail * feat(menu): code cleanup, rename types to include media * feat(menu): add use case for playlist explicitly * feat(menu): return empty string for default case * feat(menu): rename constant --- packages/common/src/constants.ts | 3 ++- packages/common/src/utils/urlFormatting.ts | 20 +++++++++++++++++++ packages/common/types/config.ts | 14 +++++++------ packages/hooks-react/src/usePlaylist.ts | 12 +++++------ packages/hooks-react/src/usePlaylists.ts | 4 ++-- .../ui-react/src/components/Shelf/Shelf.tsx | 4 ++-- .../ui-react/src/containers/Layout/Layout.tsx | 5 ++--- .../SidebarContainer/SidebarContainer.tsx | 6 +++--- .../ScreenRouting/PlaylistScreenRouter.tsx | 8 ++++---- .../src/containers/AppRoutes/AppRoutes.tsx | 6 +++--- 10 files changed, 52 insertions(+), 30 deletions(-) diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index b63801922..6ad7b7a29 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -86,9 +86,10 @@ export const EPG_TYPE = { viewNexa: 'viewnexa', } as const; -export const PLAYLIST_TYPE = { +export const APP_CONFIG_ITEM_TYPE = { playlist: 'playlist', continue_watching: 'continue_watching', favorites: 'favorites', content_list: 'content_list', + media: 'media', } as const; diff --git a/packages/common/src/utils/urlFormatting.ts b/packages/common/src/utils/urlFormatting.ts index 09994e790..cc968c82f 100644 --- a/packages/common/src/utils/urlFormatting.ts +++ b/packages/common/src/utils/urlFormatting.ts @@ -1,6 +1,9 @@ +import type { AppMenuType } from '@jwp/ott-common/types/config'; + import type { PlaylistItem } from '../../types/playlist'; import { PATH_MEDIA, PATH_PLAYLIST, PATH_CONTENT_LIST } from '../paths'; import { logWarn } from '../logger'; +import { APP_CONFIG_ITEM_TYPE } from '../constants'; import { getLegacySeriesPlaylistIdFromEpisodeTags, getSeriesPlaylistIdFromCustomParams } from './media'; @@ -117,6 +120,10 @@ export const mediaURL = ({ ); }; +export const singleMediaURL = (id: string, title?: string) => { + return createPath(PATH_MEDIA, { id, title: title ? slugify(title) : undefined }); +}; + export const playlistURL = (id: string, title?: string) => { return createPath(PATH_PLAYLIST, { id, title: title ? slugify(title) : undefined }); }; @@ -125,6 +132,19 @@ export const contentListURL = (id: string, title?: string) => { return createPath(PATH_CONTENT_LIST, { id, title: title ? slugify(title) : undefined }); }; +export const determinePath = ({ type, contentId }: { type: AppMenuType | undefined; contentId: string }) => { + switch (type) { + case APP_CONFIG_ITEM_TYPE.content_list: + return contentListURL(contentId); + case APP_CONFIG_ITEM_TYPE.media: + return singleMediaURL(contentId); + case APP_CONFIG_ITEM_TYPE.playlist: + return playlistURL(contentId); + default: + return ''; + } +}; + export const liveChannelsURL = (playlistId: string, channelId?: string, play = false) => { return createPath( PATH_PLAYLIST, diff --git a/packages/common/types/config.ts b/packages/common/types/config.ts index d3b62cfe9..2d8c980ee 100644 --- a/packages/common/types/config.ts +++ b/packages/common/types/config.ts @@ -1,4 +1,4 @@ -import type { PLAYLIST_TYPE } from '../src/constants'; +import type { APP_CONFIG_ITEM_TYPE } from '../src/constants'; import type { AdScheduleUrls, AdDeliveryMethod } from './ad-schedule'; @@ -43,14 +43,14 @@ export type Drm = { defaultPolicyId: string; }; -export type PlaylistType = keyof typeof PLAYLIST_TYPE; - -export type PlaylistMenuType = Extract; +export type AppContentType = keyof typeof APP_CONFIG_ITEM_TYPE; +export type AppMenuType = Extract; +export type AppShelfType = Extract; export type Content = { contentId?: string; title?: string; - type: PlaylistType; + type: AppShelfType; featured?: boolean; backgroundColor?: string | null; }; @@ -58,7 +58,7 @@ export type Content = { export type Menu = { label: string; contentId: string; - type?: PlaylistMenuType; + type?: AppMenuType; filterTags?: string; }; @@ -78,11 +78,13 @@ export type Cleeng = { yearlyOffer?: string | null; useSandbox?: boolean; }; + export type JWP = { clientId?: string | null; assetId?: number | null; useSandbox?: boolean; }; + export type Features = { recommendationsPlaylist?: string | null; searchPlaylist?: string | null; diff --git a/packages/hooks-react/src/usePlaylist.ts b/packages/hooks-react/src/usePlaylist.ts index 9637731c1..5e8ed77b5 100644 --- a/packages/hooks-react/src/usePlaylist.ts +++ b/packages/hooks-react/src/usePlaylist.ts @@ -6,8 +6,8 @@ import { generatePlaylistPlaceholder } from '@jwp/ott-common/src/utils/collectio import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; import { isTruthyCustomParamValue } from '@jwp/ott-common/src/utils/common'; import type { ApiError } from '@jwp/ott-common/src/utils/api'; -import type { PlaylistMenuType } from '@jwp/ott-common/types/config'; -import { PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; +import type { AppMenuType } from '@jwp/ott-common/types/config'; +import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; const placeholderData = generatePlaylistPlaceholder(30); @@ -21,7 +21,7 @@ export const getPlaylistQueryOptions = ({ params = {}, queryClient, }: { - type: PlaylistMenuType; + type: AppMenuType; contentId: string | undefined; siteId: string; enabled: boolean; @@ -35,7 +35,7 @@ export const getPlaylistQueryOptions = ({ enabled: !!contentId && enabled, queryKey: ['playlist', type, contentId, params], queryFn: async () => { - if (type === PLAYLIST_TYPE.playlist) { + if (type === APP_CONFIG_ITEM_TYPE.playlist) { const playlist = await apiService.getPlaylistById(contentId, params); // This pre-caches all playlist items and makes navigating a lot faster. @@ -44,7 +44,7 @@ export const getPlaylistQueryOptions = ({ }); return playlist; - } else if (type === PLAYLIST_TYPE.content_list) { + } else if (type === APP_CONFIG_ITEM_TYPE.content_list) { const contentList = await apiService.getContentList({ siteId, id: contentId }); return contentList; @@ -67,7 +67,7 @@ export default function usePlaylist( params: GetPlaylistParams = {}, enabled: boolean = true, usePlaceholderData: boolean = true, - type: PlaylistMenuType = PLAYLIST_TYPE.playlist, + type: AppMenuType = APP_CONFIG_ITEM_TYPE.playlist, ) { const queryClient = useQueryClient(); const siteId = useConfigStore((state) => state.config.siteId); diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts index 3b0d6c7f5..14e658246 100644 --- a/packages/hooks-react/src/usePlaylists.ts +++ b/packages/hooks-react/src/usePlaylists.ts @@ -2,7 +2,7 @@ import { PersonalShelf, PersonalShelves, PLAYLIST_LIMIT } from '@jwp/ott-common/ import { useFavoritesStore } from '@jwp/ott-common/src/stores/FavoritesStore'; import { useWatchHistoryStore } from '@jwp/ott-common/src/stores/WatchHistoryStore'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; -import type { Content, PlaylistMenuType, PlaylistType } from '@jwp/ott-common/types/config'; +import type { Content, AppContentType, AppMenuType } from '@jwp/ott-common/types/config'; import type { Playlist } from '@jwp/ott-common/types/playlist'; import { useQueries, useQueryClient } from 'react-query'; @@ -15,7 +15,7 @@ type UsePlaylistResult = { isPlaceholderData?: boolean; }[]; -const isPlaylistType = (type: PlaylistType): type is PlaylistMenuType => !PersonalShelves.some((pType) => pType === type); +const isPlaylistType = (type: AppContentType): type is AppMenuType => !PersonalShelves.some((pType) => pType === type); const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undefined) => { const page_limit = PLAYLIST_LIMIT.toString(); diff --git a/packages/ui-react/src/components/Shelf/Shelf.tsx b/packages/ui-react/src/components/Shelf/Shelf.tsx index cec054ba7..92c9eb2d6 100644 --- a/packages/ui-react/src/components/Shelf/Shelf.tsx +++ b/packages/ui-react/src/components/Shelf/Shelf.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { CYCLE_MODE_RESTART, type RenderControl, type RenderPagination, TileSlider } from '@videodock/tile-slider'; import type { Playlist, PlaylistItem } from '@jwp/ott-common/types/playlist'; -import type { AccessModel, PlaylistType } from '@jwp/ott-common/types/config'; +import type { AccessModel, AppContentType } from '@jwp/ott-common/types/config'; import { isLocked } from '@jwp/ott-common/src/utils/entitlements'; import { mediaURL } from '@jwp/ott-common/src/utils/urlFormatting'; import { PersonalShelf } from '@jwp/ott-common/src/constants'; @@ -39,7 +39,7 @@ export const ShelfIdentifier = Symbol(`SHELF`); export type ShelfProps = { playlist: Playlist; - type: PlaylistType; + type: AppContentType; onCardHover?: (playlistItem: PlaylistItem) => void; watchHistory?: { [key: string]: number }; enableTitle?: boolean; diff --git a/packages/ui-react/src/containers/Layout/Layout.tsx b/packages/ui-react/src/containers/Layout/Layout.tsx index 5296ed960..ce727d2fb 100644 --- a/packages/ui-react/src/containers/Layout/Layout.tsx +++ b/packages/ui-react/src/containers/Layout/Layout.tsx @@ -4,10 +4,9 @@ import { Outlet } from 'react-router'; import { shallow } from '@jwp/ott-common/src/utils/compare'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { unicodeToChar } from '@jwp/ott-common/src/utils/common'; -import { contentListURL, playlistURL } from '@jwp/ott-common/src/utils/urlFormatting'; +import { determinePath } from '@jwp/ott-common/src/utils/urlFormatting'; import env from '@jwp/ott-common/src/env'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; -import { PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; import Header from '../../components/Header/Header'; import Footer from '../../components/Footer/Footer'; @@ -51,7 +50,7 @@ const Layout = () => { { label: t('home'), to: '/' }, ...menu.map(({ label, contentId, type }) => ({ label, - to: type === PLAYLIST_TYPE.content_list ? contentListURL(contentId) : playlistURL(contentId), + to: determinePath({ type, contentId }), })), ]; diff --git a/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx b/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx index 527c412ff..47b31029a 100644 --- a/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx +++ b/packages/ui-react/src/containers/SidebarContainer/SidebarContainer.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { contentListURL, playlistURL } from '@jwp/ott-common/src/utils/urlFormatting'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import useOpaqueId from '@jwp/ott-hooks-react/src/useOpaqueId'; import { useLocation, useNavigate } from 'react-router'; -import { ACCESS_MODEL, PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; +import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; +import { determinePath } from '@jwp/ott-common/src/utils/urlFormatting'; import Button from '../../components/Button/Button'; import Sidebar from '../../components/Sidebar/Sidebar'; @@ -84,7 +84,7 @@ const SidebarContainer = () => { {menu.map(({ contentId, type, label }) => (
  • - +
  • ))} diff --git a/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx b/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx index 6f12fbe70..1663b49a3 100644 --- a/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import type { Playlist } from '@jwp/ott-common/types/playlist'; -import { PLAYLIST_TYPE, PLAYLIST_CONTENT_TYPE } from '@jwp/ott-common/src/constants'; +import { APP_CONFIG_ITEM_TYPE, PLAYLIST_CONTENT_TYPE } from '@jwp/ott-common/src/constants'; import { ScreenMap } from '@jwp/ott-common/src/utils/ScreenMap'; import usePlaylist from '@jwp/ott-hooks-react/src/usePlaylist'; -import type { PlaylistMenuType } from '@jwp/ott-common/types/config'; +import type { AppMenuType } from '@jwp/ott-common/types/config'; import Loading from '../Loading/Loading'; import ErrorPage from '../../components/ErrorPage/ErrorPage'; @@ -24,7 +24,7 @@ playlistScreenMap.registerByContentType(PlaylistLiveChannels, PLAYLIST_CONTENT_T // register content list screens contentScreenMap.registerDefault(PlaylistGrid); -const PlaylistScreenRouter = ({ type }: { type: PlaylistMenuType }) => { +const PlaylistScreenRouter = ({ type }: { type: AppMenuType }) => { const params = useParams(); const id = params.id || ''; @@ -43,7 +43,7 @@ const PlaylistScreenRouter = ({ type }: { type: PlaylistMenuType }) => { return ; } - const Screen = type === PLAYLIST_TYPE.content_list ? contentScreenMap.getScreen(data) : playlistScreenMap.getScreen(data); + const Screen = type === APP_CONFIG_ITEM_TYPE.content_list ? contentScreenMap.getScreen(data) : playlistScreenMap.getScreen(data); return ; }; diff --git a/platforms/web/src/containers/AppRoutes/AppRoutes.tsx b/platforms/web/src/containers/AppRoutes/AppRoutes.tsx index 4d71c4739..74a964a3d 100644 --- a/platforms/web/src/containers/AppRoutes/AppRoutes.tsx +++ b/platforms/web/src/containers/AppRoutes/AppRoutes.tsx @@ -12,7 +12,7 @@ import MediaScreenRouter from '@jwp/ott-ui-react/src/pages/ScreenRouting/MediaSc import PlaylistScreenRouter from '@jwp/ott-ui-react/src/pages/ScreenRouting/PlaylistScreenRouter'; import Layout from '@jwp/ott-ui-react/src/containers/Layout/Layout'; import { PATH_ABOUT, PATH_CONTENT_LIST, PATH_LEGACY_SERIES, PATH_MEDIA, PATH_PLAYLIST, PATH_SEARCH, PATH_USER } from '@jwp/ott-common/src/paths'; -import { PLAYLIST_TYPE } from '@jwp/ott-common/src/constants'; +import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import RoutesContainer from '#src/containers/RoutesContainer/RoutesContainer'; @@ -24,8 +24,8 @@ export default function AppRoutes() { }> } errorElement={}> } /> - } /> - } /> + } /> + } /> } /> } /> } /> From e116b1274f63d893b920957d26172248c097cd91 Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:39:46 +0300 Subject: [PATCH 03/16] [OWA-79] feat(i18n): add translations (#602) * feat(i18n): add custom params translations for media title and description * feat(i18n): add custom params translations for all translatable fields * feat(i18n): update getTranslatableFields function * feat(i18n): clean up getTranslatedFields function * feat(i18n): make language optional, clean up code * feat(i18n): remove hardcoded default language en and replace with env variable * feat(i18n): pass language to usePlaylist * feat(i18n): add language for useSearch * feat(i18n): add language to query key * feat(i18n): remove custom built hook, use i18n.language instead * feat(i18n): get language directly inside the hooks * feat(i18n): revert usePlaylist function signature * feat(i18n): revert usePlaylist to initial code usage * feat(i18n): revert function signature for useMedia * feat(i18n): fix order for code * feat(i18n): remove language import for useProtectedMedia * feat(i18n): use local storage for favorites, watchlist in combination with page reload * feat(i18n): use i18next instead of local storage * feat(i18n): remove reload code from language switcher * feat(i18n): fix language switch for favorites and continue watching * feat(i18n): pass language for app init as well * feat(i18n): clean up code * feat(i18n): fix test fail * feat(i18n): rename variable to better reflect function utility * feat(i18n): use i18n directly * feat(i18n): clean up functions * feat(i18n): fix getMediaById * feat(i18n): code cleanup * feat(i18n): clean up functions signatures * feat(i18n): get language directly in watchlist initialize function * feat(i18n): revert change for initI18n * feat(i18n): revert small formatting changes, irrelevant for this task * feat(i18n): revert to using useTranslation hook * feat(i18n): remove unnecessary type cast * feat(i18n): remove menu from useTranslation --- .../common/src/controllers/AppController.ts | 6 +- .../src/controllers/FavoritesController.ts | 8 +- .../src/controllers/WatchHistoryController.ts | 8 +- packages/common/src/services/ApiService.ts | 82 ++++++++++++++----- .../common/src/services/FavoriteService.ts | 4 +- .../src/services/WatchHistoryService.ts | 19 +++-- .../hooks-react/src/series/useEpisodes.ts | 11 ++- .../hooks-react/src/series/useNextEpisode.ts | 9 +- packages/hooks-react/src/useBootstrapApp.ts | 5 +- packages/hooks-react/src/useMedia.ts | 7 +- packages/hooks-react/src/usePlaylist.ts | 15 +++- packages/hooks-react/src/usePlaylists.ts | 5 ++ packages/hooks-react/src/useProtectedMedia.ts | 4 +- .../HeaderLanguageSwitcher.tsx | 8 ++ .../src/containers/Layout/Layout.test.tsx | 4 + 15 files changed, 142 insertions(+), 53 deletions(-) diff --git a/packages/common/src/controllers/AppController.ts b/packages/common/src/controllers/AppController.ts index 6d86d6d36..474d04c86 100644 --- a/packages/common/src/controllers/AppController.ts +++ b/packages/common/src/controllers/AppController.ts @@ -59,7 +59,7 @@ export default class AppController { return config; }; - initializeApp = async (url: string, refreshEntitlements?: () => Promise) => { + initializeApp = async (url: string, language: string, refreshEntitlements?: () => Promise) => { logDebug('AppController', 'Initializing app', { url }); const settings = await this.settingsService.initialize(); @@ -83,11 +83,11 @@ export default class AppController { } if (config.features?.continueWatchingList && config.content.some((el) => el.type === PersonalShelf.ContinueWatching)) { - await getModule(WatchHistoryController).initialize(); + await getModule(WatchHistoryController).initialize(language); } if (config.features?.favoritesList && config.content.some((el) => el.type === PersonalShelf.Favorites)) { - await getModule(FavoritesController).initialize(); + await getModule(FavoritesController).initialize(language); } return { config, settings, configSource }; diff --git a/packages/common/src/controllers/FavoritesController.ts b/packages/common/src/controllers/FavoritesController.ts index e871bcf63..9dc9bdf76 100644 --- a/packages/common/src/controllers/FavoritesController.ts +++ b/packages/common/src/controllers/FavoritesController.ts @@ -15,11 +15,11 @@ export default class FavoritesController { this.favoritesService = favoritesService; } - initialize = async () => { - await this.restoreFavorites(); + initialize = async (language: string) => { + await this.restoreFavorites(language); }; - restoreFavorites = async () => { + restoreFavorites = async (language?: string) => { const { user } = useAccountStore.getState(); const favoritesList = useConfigStore.getState().config.features?.favoritesList; @@ -27,7 +27,7 @@ export default class FavoritesController { return; } - const favorites = await this.favoritesService.getFavorites(user, favoritesList); + const favorites = await this.favoritesService.getFavorites(user, favoritesList, language); useFavoritesStore.setState({ favorites, favoritesPlaylistId: favoritesList }); }; diff --git a/packages/common/src/controllers/WatchHistoryController.ts b/packages/common/src/controllers/WatchHistoryController.ts index 708914ec1..e433bfa7f 100644 --- a/packages/common/src/controllers/WatchHistoryController.ts +++ b/packages/common/src/controllers/WatchHistoryController.ts @@ -15,11 +15,11 @@ export default class WatchHistoryController { this.watchHistoryService = watchHistoryService; } - initialize = async () => { - await this.restoreWatchHistory(); + initialize = async (language: string) => { + await this.restoreWatchHistory(language); }; - restoreWatchHistory = async () => { + restoreWatchHistory = async (language?: string) => { const { user } = useAccountStore.getState(); const continueWatchingList = useConfigStore.getState().config.features?.continueWatchingList; @@ -27,7 +27,7 @@ export default class WatchHistoryController { return; } - const watchHistory = await this.watchHistoryService.getWatchHistory(user, continueWatchingList); + const watchHistory = await this.watchHistoryService.getWatchHistory(user, continueWatchingList, language); useWatchHistoryStore.setState({ watchHistory: watchHistory.filter((item): item is WatchHistoryItem => !!item?.mediaid), diff --git a/packages/common/src/services/ApiService.ts b/packages/common/src/services/ApiService.ts index 6cf199d1d..0089768e2 100644 --- a/packages/common/src/services/ApiService.ts +++ b/packages/common/src/services/ApiService.ts @@ -46,10 +46,29 @@ export default class ApiService { return date ? parseISO(date) : undefined; }; + protected getTranslatedFields = (item: PlaylistItem, language?: string) => { + if (!language) { + return item; + } + + const defaultLanguage = env.APP_DEFAULT_LANGUAGE; + const transformedItem = { ...item }; + + if (language !== defaultLanguage) { + for (const [key, _] of Object.entries(transformedItem)) { + if (item[`${key}-${language}`]) { + transformedItem[key] = item[`${key}-${language}`]; + } + } + } + + return transformedItem; + }; + /** * Transform incoming content lists */ - protected transformContentList = (contentList: ContentList): Playlist => { + protected transformContentList = (contentList: ContentList, language: string): Playlist => { const { list, ...rest } = contentList; const playlist: Playlist = { ...rest, playlist: [] }; @@ -71,7 +90,7 @@ export default class ApiService { ...custom_params, }; - return this.transformMediaItem(playlistItem, playlist); + return this.transformMediaItem({ item: playlistItem, playlist, language }); }); return playlist; @@ -80,8 +99,8 @@ export default class ApiService { /** * Transform incoming playlists */ - protected transformPlaylist = (playlist: Playlist, relatedMediaId?: string) => { - playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem(item, playlist)); + protected transformPlaylist = (playlist: Playlist, relatedMediaId?: string, language?: string) => { + playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem({ item, playlist, language })); // remove the related media item (when this is a recommendations playlist) if (relatedMediaId) { @@ -95,14 +114,16 @@ export default class ApiService { * Transform incoming media items * - Parses productId into MediaOffer[] for all cleeng offers */ - transformMediaItem = (item: PlaylistItem, playlist?: Playlist) => { + transformMediaItem = ({ item, playlist, language }: { item: PlaylistItem; playlist?: Playlist; language?: string }) => { const config = ConfigStore.getState().config; const offerKeys = Object.keys(config?.integrations)[0]; const playlistLabel = playlist?.imageLabel; const mediaId = item.mediaid; + const translatedFields = this.getTranslatedFields(item, language); const transformedMediaItem = { ...item, + ...translatedFields, cardImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.CARD, playlistLabel }), channelLogoImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.CHANNEL_LOGO, playlistLabel }), backgroundImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.BACKGROUND }), @@ -117,7 +138,7 @@ export default class ApiService { return transformedMediaItem; }; - private transformEpisodes = (episodesRes: EpisodesRes, seasonNumber?: number) => { + private transformEpisodes = (episodesRes: EpisodesRes, language: string, seasonNumber?: number) => { const { episodes, page, page_limit, total } = episodesRes; // Adding images and keys for media items @@ -125,7 +146,7 @@ export default class ApiService { episodes: episodes .filter((el) => el.media_item) .map((el) => ({ - ...this.transformMediaItem(el.media_item as PlaylistItem), + ...this.transformMediaItem({ item: el.media_item as PlaylistItem, language }), seasonNumber: seasonNumber?.toString() || el.season_number?.toString() || '', episodeNumber: String(el.episode_number), })), @@ -136,7 +157,17 @@ export default class ApiService { /** * Get watchlist by playlistId */ - getMediaByWatchlist = async (playlistId: string, mediaIds: string[], token?: string): Promise => { + getMediaByWatchlist = async ({ + playlistId, + mediaIds, + token, + language, + }: { + playlistId: string; + mediaIds: string[]; + token?: string; + language?: string; + }): Promise => { if (!mediaIds?.length) { return []; } @@ -148,16 +179,23 @@ export default class ApiService { if (!data) throw new Error(`The data was not found using the watchlist ${playlistId}`); - return (data.playlist || []).map((item) => this.transformMediaItem(item)); + return (data.playlist || []).map((item) => this.transformMediaItem({ item, language })); }; /** * Get media by id - * @param {string} id - * @param {string} [token] - * @param {string} [drmPolicyId] */ - getMediaById = async (id: string, token?: string, drmPolicyId?: string): Promise => { + getMediaById = async ({ + id, + token, + drmPolicyId, + language, + }: { + id: string; + token?: string; + drmPolicyId?: string; + language?: string; + }): Promise => { const pathname = drmPolicyId ? `/v2/media/${id}/drm/${drmPolicyId}` : `/v2/media/${id}`; const url = createURL(`${env.APP_API_BASE_URL}${pathname}`, { token }); const response = await fetch(url); @@ -166,7 +204,7 @@ export default class ApiService { if (!mediaItem) throw new Error('MediaItem not found'); - return this.transformMediaItem(mediaItem); + return this.transformMediaItem({ item: mediaItem, language }); }; /** @@ -205,11 +243,13 @@ export default class ApiService { pageOffset, pageLimit = PAGE_LIMIT, afterId, + language, }: { seriesId: string | undefined; pageOffset?: number; pageLimit?: number; afterId?: string; + language: string; }): Promise => { if (!seriesId) { throw new Error('Series ID is required'); @@ -225,7 +265,7 @@ export default class ApiService { const response = await fetch(url); const episodesResponse = (await getDataOrThrow(response)) as EpisodesRes; - return this.transformEpisodes(episodesResponse); + return this.transformEpisodes(episodesResponse, language); }; /** @@ -236,8 +276,10 @@ export default class ApiService { seasonNumber, pageOffset, pageLimit = PAGE_LIMIT, + language, }: { seriesId: string | undefined; + language: string; seasonNumber: number; pageOffset?: number; pageLimit?: number; @@ -252,7 +294,7 @@ export default class ApiService { const response = await fetch(url); const episodesRes = (await getDataOrThrow(response)) as EpisodesRes; - return this.transformEpisodes(episodesRes, seasonNumber); + return this.transformEpisodes(episodesRes, language, seasonNumber); }; getAdSchedule = async (id: string | undefined | null): Promise => { @@ -269,7 +311,7 @@ export default class ApiService { /** * Get playlist by id */ - getPlaylistById = async (id?: string, params: GetPlaylistParams = {}): Promise => { + getPlaylistById = async (id?: string, params: GetPlaylistParams = {}, language: string = env.APP_DEFAULT_LANGUAGE): Promise => { if (!id) { return undefined; } @@ -279,10 +321,10 @@ export default class ApiService { const response = await fetch(url); const data = (await getDataOrThrow(response)) as Playlist; - return this.transformPlaylist(data, params.related_media_id); + return this.transformPlaylist(data, params.related_media_id, language); }; - getContentList = async ({ id, siteId }: { id: string | undefined; siteId: string }): Promise => { + getContentList = async ({ id, siteId, language }: { id: string | undefined; siteId: string; language: string }): Promise => { if (!id || !siteId) { throw new Error('List ID and Site ID are required'); } @@ -292,7 +334,7 @@ export default class ApiService { const response = await fetch(url); const data = (await getDataOrThrow(response)) as ContentList; - return this.transformContentList(data); + return this.transformContentList(data, language); }; getContentSearch = async ({ siteId, params }: { siteId: string; params: GetContentSearchParams }) => { diff --git a/packages/common/src/services/FavoriteService.ts b/packages/common/src/services/FavoriteService.ts index b35be09b5..b6eb968f6 100644 --- a/packages/common/src/services/FavoriteService.ts +++ b/packages/common/src/services/FavoriteService.ts @@ -57,7 +57,7 @@ export default class FavoriteService { return this.validateFavorites(favorites); } - getFavorites = async (user: Customer | null, favoritesList: string) => { + getFavorites = async (user: Customer | null, favoritesList: string, language?: string) => { const savedItems = user ? await this.getFavoritesFromAccount(user) : await this.getFavoritesFromStorage(); const mediaIds = savedItems.map(({ mediaid }) => mediaid); @@ -66,7 +66,7 @@ export default class FavoriteService { } try { - const playlistItems = await this.apiService.getMediaByWatchlist(favoritesList, mediaIds); + const playlistItems = await this.apiService.getMediaByWatchlist({ playlistId: favoritesList, mediaIds, language }); return (playlistItems || []).map((item) => this.createFavorite(item)); } catch (error: unknown) { diff --git a/packages/common/src/services/WatchHistoryService.ts b/packages/common/src/services/WatchHistoryService.ts index 80ec9d56a..c8beaa396 100644 --- a/packages/common/src/services/WatchHistoryService.ts +++ b/packages/common/src/services/WatchHistoryService.ts @@ -39,22 +39,25 @@ export default class WatchHistoryService { } // Retrieve watch history media items info using a provided watch list - protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[]): Promise> => { - const watchHistoryItems = await this.apiService.getMediaByWatchlist(continueWatchingList, ids); + protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[], language?: string): Promise> => { + const watchHistoryItems = await this.apiService.getMediaByWatchlist({ playlistId: continueWatchingList, mediaIds: ids, language }); const watchHistoryItemsDict = Object.fromEntries((watchHistoryItems || []).map((item) => [item.mediaid, item])); return watchHistoryItemsDict; }; // We store separate episodes in the watch history and to show series card in the Continue Watching shelf we need to get their parent media items - protected getWatchHistorySeriesItems = async (continueWatchingList: string, ids: string[]): Promise> => { + protected getWatchHistorySeriesItems = async ( + continueWatchingList: string, + ids: string[], + language?: string, + ): Promise> => { const mediaWithSeries = await this.apiService.getSeriesByMediaIds(ids); const seriesIds = Object.keys(mediaWithSeries || {}) .map((key) => mediaWithSeries?.[key]?.[0]?.series_id) .filter(Boolean) as string[]; const uniqueSerieIds = [...new Set(seriesIds)]; - - const seriesItems = await this.apiService.getMediaByWatchlist(continueWatchingList, uniqueSerieIds); + const seriesItems = await this.apiService.getMediaByWatchlist({ playlistId: continueWatchingList, mediaIds: uniqueSerieIds, language }); const seriesItemsDict = Object.keys(mediaWithSeries || {}).reduce((acc, key) => { const seriesItemId = mediaWithSeries?.[key]?.[0]?.series_id; if (seriesItemId) { @@ -86,7 +89,7 @@ export default class WatchHistoryService { return this.validateWatchHistory(history); } - getWatchHistory = async (user: Customer | null, continueWatchingList: string) => { + getWatchHistory = async (user: Customer | null, continueWatchingList: string, language?: string) => { const savedItems = user ? await this.getWatchHistoryFromAccount(user) : await this.getWatchHistoryFromStorage(); // When item is an episode of the new flow -> show the card as a series one, but keep episode to redirect in a right way @@ -97,8 +100,8 @@ export default class WatchHistoryService { } try { - const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids); - const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids); + const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids, language); + const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids, language); return savedItems .map((item) => { diff --git a/packages/hooks-react/src/series/useEpisodes.ts b/packages/hooks-react/src/series/useEpisodes.ts index 7d91fa9e2..76a29aa93 100644 --- a/packages/hooks-react/src/series/useEpisodes.ts +++ b/packages/hooks-react/src/series/useEpisodes.ts @@ -4,6 +4,7 @@ import type { Pagination } from '@jwp/ott-common/types/pagination'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; const getNextPageParam = (pagination: Pagination) => { const { page, page_limit, total } = pagination; @@ -28,22 +29,26 @@ export const useEpisodes = ( } => { const apiService = getModule(ApiService); + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const { data, fetchNextPage, isLoading, hasNextPage = false, } = useInfiniteQuery( - [seriesId, seasonNumber], + [seriesId, seasonNumber, language], async ({ pageParam = 0 }) => { if (Number(seasonNumber)) { // Get episodes from a selected season using pagination - const season = await apiService.getSeasonWithEpisodes({ seriesId, seasonNumber: Number(seasonNumber), pageOffset: pageParam }); + const season = await apiService.getSeasonWithEpisodes({ seriesId, seasonNumber: Number(seasonNumber), pageOffset: pageParam, language }); return { pagination: season.pagination, episodes: season.episodes }; } else { // Get episodes from a selected series using pagination - const data = await apiService.getEpisodes({ seriesId, pageOffset: pageParam }); + const data = await apiService.getEpisodes({ seriesId, pageOffset: pageParam, language }); return data; } }, diff --git a/packages/hooks-react/src/series/useNextEpisode.ts b/packages/hooks-react/src/series/useNextEpisode.ts index 2a33d573c..ea97a0c3f 100644 --- a/packages/hooks-react/src/series/useNextEpisode.ts +++ b/packages/hooks-react/src/series/useNextEpisode.ts @@ -3,14 +3,19 @@ import type { Series } from '@jwp/ott-common/types/series'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; export const useNextEpisode = ({ series, episodeId }: { series: Series | undefined; episodeId: string | undefined }) => { const apiService = getModule(ApiService); + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const { isLoading, data } = useQuery( - ['next-episode', series?.series_id, episodeId], + ['next-episode', series?.series_id, episodeId, language], async () => { - const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, afterId: episodeId }); + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, afterId: episodeId, language }); return item?.episodes?.[0]; }, diff --git a/packages/hooks-react/src/useBootstrapApp.ts b/packages/hooks-react/src/useBootstrapApp.ts index 6c366dc21..ad85e2564 100644 --- a/packages/hooks-react/src/useBootstrapApp.ts +++ b/packages/hooks-react/src/useBootstrapApp.ts @@ -6,6 +6,7 @@ import AppController from '@jwp/ott-common/src/controllers/AppController'; import type { AppError } from '@jwp/ott-common/src/utils/error'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; import { logDebug } from '@jwp/ott-common/src/logger'; +import { useTranslation } from 'react-i18next'; const applicationController = getModule(AppController); @@ -19,11 +20,13 @@ export type OnReadyCallback = (config: Config | undefined) => void; export const useBootstrapApp = (url: string, onReady: OnReadyCallback) => { const queryClient = useQueryClient(); + const { i18n } = useTranslation(); + const refreshEntitlements = () => queryClient.invalidateQueries({ queryKey: ['entitlements'] }); const { data, isLoading, error, isSuccess, refetch } = useQuery( 'config-init', - () => applicationController.initializeApp(url, refreshEntitlements), + () => applicationController.initializeApp(url, i18n.language, refreshEntitlements), { refetchInterval: false, retry: 1, diff --git a/packages/hooks-react/src/useMedia.ts b/packages/hooks-react/src/useMedia.ts index a3cd68aa3..78de95e3d 100644 --- a/packages/hooks-react/src/useMedia.ts +++ b/packages/hooks-react/src/useMedia.ts @@ -3,13 +3,18 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; +import { useTranslation } from 'react-i18next'; export type UseMediaResult = UseBaseQueryResult; export default function useMedia(mediaId: string, enabled: boolean = true): UseMediaResult { const apiService = getModule(ApiService); - return useQuery(['media', mediaId], () => apiService.getMediaById(mediaId), { + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + + return useQuery(['media', mediaId, language], () => apiService.getMediaById({ id: mediaId, language }), { enabled: !!mediaId && enabled, refetchInterval: (data, _) => { if (!data) return false; diff --git a/packages/hooks-react/src/usePlaylist.ts b/packages/hooks-react/src/usePlaylist.ts index 5e8ed77b5..6768653f7 100644 --- a/packages/hooks-react/src/usePlaylist.ts +++ b/packages/hooks-react/src/usePlaylist.ts @@ -9,6 +9,7 @@ import type { ApiError } from '@jwp/ott-common/src/utils/api'; import type { AppMenuType } from '@jwp/ott-common/types/config'; import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { useTranslation } from 'react-i18next'; const placeholderData = generatePlaylistPlaceholder(30); @@ -20,6 +21,7 @@ export const getPlaylistQueryOptions = ({ usePlaceholderData, params = {}, queryClient, + language, }: { type: AppMenuType; contentId: string | undefined; @@ -28,15 +30,16 @@ export const getPlaylistQueryOptions = ({ queryClient: QueryClient; usePlaceholderData?: boolean; params?: GetPlaylistParams; + language: string; }) => { const apiService = getModule(ApiService); return { enabled: !!contentId && enabled, - queryKey: ['playlist', type, contentId, params], + queryKey: ['playlist', type, contentId, params, language], queryFn: async () => { if (type === APP_CONFIG_ITEM_TYPE.playlist) { - const playlist = await apiService.getPlaylistById(contentId, params); + const playlist = await apiService.getPlaylistById(contentId, params, language); // This pre-caches all playlist items and makes navigating a lot faster. playlist?.playlist?.forEach((playlistItem) => { @@ -45,7 +48,7 @@ export const getPlaylistQueryOptions = ({ return playlist; } else if (type === APP_CONFIG_ITEM_TYPE.content_list) { - const contentList = await apiService.getContentList({ siteId, id: contentId }); + const contentList = await apiService.getContentList({ siteId, id: contentId, language }); return contentList; } @@ -69,10 +72,14 @@ export default function usePlaylist( usePlaceholderData: boolean = true, type: AppMenuType = APP_CONFIG_ITEM_TYPE.playlist, ) { + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const queryClient = useQueryClient(); const siteId = useConfigStore((state) => state.config.siteId); - const queryOptions = getPlaylistQueryOptions({ type, contentId, siteId, params, queryClient, enabled, usePlaceholderData }); + const queryOptions = getPlaylistQueryOptions({ type, contentId, siteId, params, queryClient, enabled, usePlaceholderData, language }); return useQuery(queryOptions); } diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts index 14e658246..b3824ac9b 100644 --- a/packages/hooks-react/src/usePlaylists.ts +++ b/packages/hooks-react/src/usePlaylists.ts @@ -5,6 +5,7 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import type { Content, AppContentType, AppMenuType } from '@jwp/ott-common/types/config'; import type { Playlist } from '@jwp/ott-common/types/playlist'; import { useQueries, useQueryClient } from 'react-query'; +import { useTranslation } from 'react-i18next'; import { getPlaylistQueryOptions } from './usePlaylist'; @@ -25,6 +26,9 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef const favorites = useFavoritesStore((state) => state.getPlaylist()); const watchHistory = useWatchHistoryStore((state) => state.getPlaylist()); + // Determine currently selected language + const { i18n } = useTranslation(); + const playlistQueries = useQueries( content.map(({ contentId, type }, index) => { if (isPlaylistType(type)) { @@ -36,6 +40,7 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef queryClient, usePlaceholderData: true, params: { page_limit }, + language: i18n.language, }); } diff --git a/packages/hooks-react/src/useProtectedMedia.ts b/packages/hooks-react/src/useProtectedMedia.ts index c75649b95..f034e3378 100644 --- a/packages/hooks-react/src/useProtectedMedia.ts +++ b/packages/hooks-react/src/useProtectedMedia.ts @@ -7,7 +7,9 @@ import useContentProtection from './useContentProtection'; export default function useProtectedMedia(item: PlaylistItem) { const apiService = getModule(ApiService); - const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => apiService.getMediaById(item.mediaid, token, drmPolicyId)); + const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => + apiService.getMediaById({ id: item.mediaid, token, drmPolicyId }), + ); const { isLoading, data: isGeoBlocked } = useQuery( ['media', 'geo', item.mediaid], diff --git a/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx b/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx index 1513d9089..15ce22af5 100644 --- a/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx +++ b/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx @@ -2,6 +2,9 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import FavoritesController from '@jwp/ott-common/src/controllers/FavoritesController'; +import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; import LanguageMenu from '../../components/LanguageMenu/LanguageMenu'; @@ -10,11 +13,16 @@ const HeaderLanguageSwitcher = () => { const supportedLanguages = useConfigStore((state) => state.supportedLanguages); const languageMenuOpen = useUIStore((state) => state.languageMenuOpen); + const favoritesController = getModule(FavoritesController); + const watchlistController = getModule(WatchHistoryController); + const openLanguageMenu = useCallback(() => useUIStore.setState({ languageMenuOpen: true }), []); const closeLanguageMenu = useCallback(() => useUIStore.setState({ languageMenuOpen: false }), []); const languageClickHandler = (code: string) => { i18n.changeLanguage(code); + favoritesController.restoreFavorites(code); + watchlistController.restoreWatchHistory(code); }; const currentLanguage = useMemo(() => supportedLanguages.find(({ code }) => code === i18n.language), [i18n.language, supportedLanguages]); diff --git a/packages/ui-react/src/containers/Layout/Layout.test.tsx b/packages/ui-react/src/containers/Layout/Layout.test.tsx index 6ba246c09..bccd5de3f 100644 --- a/packages/ui-react/src/containers/Layout/Layout.test.tsx +++ b/packages/ui-react/src/containers/Layout/Layout.test.tsx @@ -3,6 +3,8 @@ import { axe } from 'vitest-axe'; import AccountController from '@jwp/ott-common/src/controllers/AccountController'; import { mockService } from '@jwp/ott-common/test/mockService'; import { DEFAULT_FEATURES } from '@jwp/ott-common/src/constants'; +import FavoritesController from '@jwp/ott-common/src/controllers/FavoritesController'; +import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; import { renderWithRouter } from '../../../test/utils'; @@ -11,6 +13,8 @@ import Layout from './Layout'; describe('', () => { beforeEach(() => { mockService(AccountController, { getFeatures: () => DEFAULT_FEATURES }); + mockService(FavoritesController, {}); + mockService(WatchHistoryController, {}); }); test('renders layout', () => { From ddedb6b6f636c720f0ba016b2fa07b41e964fe8e Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Thu, 12 Sep 2024 16:58:58 +0200 Subject: [PATCH 04/16] fix(series): first episode switching per season --- .../hooks-react/src/series/useFirstEpisode.ts | 24 +++++++++++ .../mediaScreens/MediaSeries/MediaSeries.tsx | 41 ++++++++++--------- 2 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 packages/hooks-react/src/series/useFirstEpisode.ts diff --git a/packages/hooks-react/src/series/useFirstEpisode.ts b/packages/hooks-react/src/series/useFirstEpisode.ts new file mode 100644 index 000000000..15d16a509 --- /dev/null +++ b/packages/hooks-react/src/series/useFirstEpisode.ts @@ -0,0 +1,24 @@ +import { useQuery } from 'react-query'; +import type { Series } from '@jwp/ott-common/types/series'; +import ApiService from '@jwp/ott-common/src/services/ApiService'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; + +export const useFirstEpisode = ({ series }: { series: Series | undefined }) => { + const apiService = getModule(ApiService); + + const { isLoading, data } = useQuery( + ['first-episode', series?.series_id], + async () => { + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1 }); + + return item?.episodes?.[0]; + }, + { staleTime: STALE_TIME, cacheTime: CACHE_TIME, enabled: !!series?.series_id }, + ); + + return { + isLoading, + data, + }; +}; diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx index 46beef251..37c617bc8 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx @@ -22,6 +22,7 @@ import { useSeriesLookup } from '@jwp/ott-hooks-react/src/series/useSeriesLookup import { useNextEpisode } from '@jwp/ott-hooks-react/src/series/useNextEpisode'; import PlayTrailer from '@jwp/ott-theme/assets/icons/play_trailer.svg?react'; import useBreakpoint, { Breakpoint } from '@jwp/ott-ui-react/src/hooks/useBreakpoint'; +import { useFirstEpisode } from '@jwp/ott-hooks-react/src/series/useFirstEpisode'; import type { ScreenComponent } from '../../../../../types/screens'; import ErrorPage from '../../../../components/ErrorPage/ErrorPage'; @@ -54,7 +55,8 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { const { isLoading: isSeriesDataLoading, data: series, error: seriesError } = useSeries(seriesId); const { isLoading: isEpisodeLoading, data: episode } = useMedia(episodeId || ''); const { isLoading: isTrailerLoading, data: trailerItem } = useMedia(episode?.trailerId || ''); - const { data: episodeInSeries, isLoading: isSeriesDictionaryLoading } = useSeriesLookup(episode?.mediaid); + const { isLoading: isSeriesDictionaryLoading, data: episodeInSeries } = useSeriesLookup(episodeId || undefined); + const { isLoading: isFirstEpisodeLoading, data: firstEpisode } = useFirstEpisode({ series }); // Whether we show series or episode information const selectedItem = (episode || seriesMedia) as PlaylistItem; @@ -76,7 +78,6 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { hasNextPage: hasNextEpisodesPage, } = useEpisodes(seriesId, seasonFilter, { enabled: seasonFilter !== undefined && !!series }); - const firstEpisode = useMemo(() => episodes?.[0]?.episodes?.[0], [episodes]); const episodeMetadata = useMemo( () => episodeInSeries && { @@ -162,22 +163,24 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { } }, [episodeMetadata, seasonFilter, isSeriesDataLoading, isEpisodeLoading, isSeriesDictionaryLoading, filters, episodeId]); + // UI + const playEpisode = episode || firstEpisode; + const isLoading = isSeriesDataLoading || isSeriesDictionaryLoading || isEpisodeLoading || isFirstEpisodeLoading; + const startWatchingButton = useMemo( - () => ( - { - setSearchParams({ ...searchParams, e: (episode || firstEpisode).mediaid, r: feedId || '', play: '1' }, { replace: true }); - }} - /> - ), - [episodeId, episode, firstEpisode, setSearchParams, searchParams, feedId], + () => + playEpisode && ( + { + setSearchParams({ ...searchParams, e: playEpisode.mediaid, r: feedId || '', play: '1' }, { replace: true }); + }} + /> + ), + [episodeId, playEpisode, setSearchParams, searchParams, feedId], ); - // UI - const isLoading = isSeriesDataLoading || isSeriesDictionaryLoading || isEpisodeLoading; - if (isLoading) return ; // Legacy series is used @@ -187,7 +190,7 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { return ; } - if (!seriesMedia || !series) return ; + if (!seriesMedia || !series || !playEpisode) return ; const pageTitle = `${selectedItem.title} - ${siteName}`; const canonicalUrl = `${window.location.origin}${mediaURL({ media: seriesMedia, episodeId: episode?.mediaid })}`; @@ -274,10 +277,10 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { hasMore={hasNextEpisodesPage} loadMore={fetchNextEpisodes} player={ - inlineLayout && (episode || firstEpisode) ? ( + inlineLayout ? ( = ({ data: seriesMedia }) => { Date: Thu, 19 Sep 2024 15:04:27 +0200 Subject: [PATCH 05/16] fix: card grid rendering previous items (#613) * fix: card grid rendering previous items * chore: remove unused code --- .../src/components/CardGrid/CardGrid.tsx | 40 +++++++++++-------- .../src/components/LayoutGrid/LayoutGrid.tsx | 11 +++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/ui-react/src/components/CardGrid/CardGrid.tsx b/packages/ui-react/src/components/CardGrid/CardGrid.tsx index 868628b92..e900a7fe1 100644 --- a/packages/ui-react/src/components/CardGrid/CardGrid.tsx +++ b/packages/ui-react/src/components/CardGrid/CardGrid.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import classNames from 'classnames'; import InfiniteScroll from 'react-infinite-scroller'; import type { Playlist, PlaylistItem } from '@jwp/ott-common/types/playlist'; @@ -46,6 +46,8 @@ export type CardGridProps = { getUrl: (item: PlaylistItem) => string; }; +const getCellKey = (item: PlaylistItem) => item.mediaid; + function CardGrid({ playlist, watchHistory, @@ -75,6 +77,25 @@ function CardGrid({ setRowCount(INITIAL_ROW_COUNT); }, [playlist.feedid]); + const renderCell = useCallback( + (playlistItem: PlaylistItem, tabIndex: number) => ( + onCardHover(playlistItem) : undefined} + loading={isLoading} + isCurrent={currentCardItem && currentCardItem.mediaid === playlistItem.mediaid} + currentLabel={currentCardLabel} + isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, playlistItem)} + posterAspect={posterAspect} + item={playlistItem} + headingLevel={headingLevel} + /> + ), + [accessModel, currentCardItem, currentCardLabel, getUrl, hasSubscription, headingLevel, isLoading, isLoggedIn, onCardHover, posterAspect, watchHistory], + ); + return ( ( - onCardHover(playlistItem) : undefined} - loading={isLoading} - isCurrent={currentCardItem && currentCardItem.mediaid === playlistItem.mediaid} - currentLabel={currentCardLabel} - isLocked={isLocked(accessModel, isLoggedIn, hasSubscription, playlistItem)} - posterAspect={posterAspect} - item={playlistItem} - headingLevel={headingLevel} - /> - )} + renderCell={renderCell} + getCellKey={getCellKey} /> ); diff --git a/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx b/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx index 385a9abab..cbf9bfcf4 100644 --- a/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx +++ b/packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx @@ -1,6 +1,6 @@ import { throttle } from '@jwp/ott-common/src/utils/common'; import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import styles from './LayoutGrid.module.scss'; @@ -9,6 +9,7 @@ type Props = { columnCount: number; data: Item[]; renderCell: (item: Item, tabIndex: number) => JSX.Element; + getCellKey: (item: Item) => string; }; const scrollIntoViewThrottled = throttle(function (focusedElement: HTMLElement) { @@ -16,7 +17,7 @@ const scrollIntoViewThrottled = throttle(function (focusedElement: HTMLElement) }, 300); // Keyboard-accessible grid layout, with focus management -const LayoutGrid = ({ className, columnCount, data, renderCell }: Props) => { +const LayoutGrid = ({ className, columnCount, data, renderCell, getCellKey }: Props) => { const [currentRowIndex, setCurrentRowIndex] = useState(0); const [currentColumnIndex, setCurrentColumnIndex] = useState(0); const gridRef = useRef(null); @@ -110,6 +111,8 @@ const LayoutGrid = ({ className, columnCount, data, renderC // eslint-disable-next-line react-hooks/exhaustive-deps }, [columnCount]); + const gridCellStyle = useMemo(() => ({ width: `${Math.round(100 / columnCount)}%` }), [columnCount]); + return (
    {Array.from({ length: rowCount }).map((_, rowIndex) => ( @@ -118,10 +121,10 @@ const LayoutGrid = ({ className, columnCount, data, renderC
    {renderCell(item, currentRowIndex === rowIndex && currentColumnIndex === columnIndex ? 0 : -1)}
    From c27a1e69c360d9ebe9abcc9cf2e8bb3a088516aa Mon Sep 17 00:00:00 2001 From: Carina Dragan Date: Thu, 19 Sep 2024 16:22:47 +0300 Subject: [PATCH 06/16] feat(i18n): fix lint error --- packages/hooks-react/src/series/useFirstEpisode.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/hooks-react/src/series/useFirstEpisode.ts b/packages/hooks-react/src/series/useFirstEpisode.ts index 15d16a509..74ff010eb 100644 --- a/packages/hooks-react/src/series/useFirstEpisode.ts +++ b/packages/hooks-react/src/series/useFirstEpisode.ts @@ -3,14 +3,16 @@ import type { Series } from '@jwp/ott-common/types/series'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; export const useFirstEpisode = ({ series }: { series: Series | undefined }) => { const apiService = getModule(ApiService); + const { i18n } = useTranslation(); const { isLoading, data } = useQuery( ['first-episode', series?.series_id], async () => { - const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1 }); + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, language: i18n.language }); return item?.episodes?.[0]; }, From aae25515bcff0e4ad8428d8b8389220eb9126d5f Mon Sep 17 00:00:00 2001 From: Kire Mitrov Date: Mon, 23 Sep 2024 12:24:14 +0200 Subject: [PATCH 07/16] feat: access bridge service (#616) * feat(project): access bridge initial structure * feat(project): yarn * feat(project): add lint-staged * feat(project): error for unused imports * feat(project): remove not needed ls-lint * feat(project): add access tests * feat(project): rename error message * feat(project): readme and docs * chore: rename project in package * feat(project): include SIMS authorization * chore: replace jw/fet with standard prettier config * chore: access-bridge changes to not trigger e2e tests * chore: revert web env * feat(project): add stripe products endpoint * chore: yarn update * feat(project): move types in common package * feat(project): update allowed methods * chore: rename stripe service fnc name * feat(project): make authorization optional * feat(project): redefine functions with obj params * chore: consistent naming convention * feat(project): add test fixtures * feat(project): add tests for stripe * feat(project): fine-tune error descriptions * feat(project): add stripe checkout * chore: move types in common * chore: move types in common * feat(project): add tests for stripe checkout * feat(project): add more tests for checkout * chore: stripe checkout params * feat(project): add test mocks class * feat(project): add metadata in stripe checkout * feat(project): add viewer from auth token, handle plan external providers * feat(project): add viewer from auth token, handle plan external providers * chore: update readme * feat(project): update docs * feat(project): add account service * chore: remove unused util fnc * feat(project): add sentry * feat(project): replace node with vite * feat(project): refactor server to use express, refactor error handling as well as tests * feat(project): add types for express * chore: add .test in the test naimings * fix: vite version mismatch * feat(project): update docs * chore: add dummy env variables in the test-unit-snapshot workflow * feat(project): refactor services, decouple plans method * feat(project): add sentry source mapping * feat(project): add sentry version and environment * chore: remove debug sentry * feat(project): additional test build for not source mapping on test * feat(project): add stripe billing portal endpoint * chore: update readme * chore: add stripe dummy secret on test-unit workflow * chore: update checkout params * feat(project): add cors middleware * feat(project): add cors middleware * feat(project): add site_id in the route * feat(project): update port it can match google cloud run reqs * feat(project): use newer version of node to make happy sentry * feat(project): move error handling to the middleware * feat(project): add refresh passport * feat(project): simplify jw error usage * feat(project): refactor routes and middlewares * feat(project): refactor structure and namings * feat(project): add missing case for refresh passport test * feat(project): load envs at runtime * fix(search): override search query cache (#594) Co-authored-by: Mike van Veenhuijzen * feat(project): app metadata insertion * chore(project): add apple smart banner tag via env var * chore(project): add android native banner data via env var * refactor(project): make code more readable * refactor(project): add itunes related application metadata * refactor(project): function rename * feat(project): add injectable wrapper to common components (#598) * refactor(project): update services members visibility and explicit inject (#590) * feat(project): remove free and productIds from content-types.json (#605) * fix(e2e): fix tests after cleeng api update (#606) * chore(release): v6.6.0 * feat(project): refactor routes and middlewares * feat(project): rebase and stuff * feat(project): add generic payment service that other services will implement * feat(project): update products to use our own types * chore: add site_id in .env.example * feat(project): add stripe error handler in middleware * chore: update readme * chore: update comments on stripe payment service * chore: remove unused type * feat(project): update sentry and logger * chore: node-version * chore: revert node version * chore: update comment on checkout controller * chore: revert node version * chore: revert env to default * feat(project): modify types for products and prices * feat(project): add deployment file and docs * chore: update readme paths * feat(project): remove mode from checkout params * feat(project): remove stripe type * chore: remove vite build file --------- Co-authored-by: langemike Co-authored-by: Mike van Veenhuijzen Co-authored-by: Roy Schut Co-authored-by: Christiaan Scheermeijer Co-authored-by: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Co-authored-by: Anton Lantukh Co-authored-by: Conventional Changelog Action --- .github/workflows/test-unit-snapshot.yml | 10 +- knip.config.ts | 1 - package.json | 3 +- packages/common/types/errors.ts | 8 + packages/common/types/passport.ts | 4 + packages/common/types/payment.ts | 49 + packages/common/types/plans.ts | 31 + platforms/access-bridge/.env.example | 37 + platforms/access-bridge/.eslintrc.cjs | 12 + platforms/access-bridge/.gitignore | 6 + platforms/access-bridge/.prettierignore | 1 + platforms/access-bridge/Makefile | 70 + platforms/access-bridge/README.md | 134 ++ platforms/access-bridge/docs/deployment.md | 50 + .../docs/developer-guidelines.md | 81 ++ platforms/access-bridge/docs/frameworks.md | 76 + .../access-bridge/lint-staged.config.cjs | 7 + platforms/access-bridge/package.json | 42 + platforms/access-bridge/prettier.config.cjs | 11 + platforms/access-bridge/src/app-config.ts | 39 + .../src/controllers/access-controller.ts | 63 + .../src/controllers/checkout-controller.ts | 71 + .../src/controllers/products-controller.ts | 32 + platforms/access-bridge/src/errors.ts | 176 +++ platforms/access-bridge/src/http.ts | 91 ++ platforms/access-bridge/src/main.ts | 47 + .../access-bridge/src/pipeline/logger.ts | 79 ++ .../access-bridge/src/pipeline/middleware.ts | 113 ++ .../access-bridge/src/pipeline/routes.ts | 37 + platforms/access-bridge/src/server.ts | 87 ++ .../src/services/identity-service.ts | 31 + .../src/services/passport-service.ts | 68 + .../src/services/payment-service.ts | 44 + .../src/services/plans-service.ts | 42 + .../src/services/stripe-payment-service.ts | 184 +++ platforms/access-bridge/stylelint.config.cjs | 3 + platforms/access-bridge/test/fixtures.ts | 200 +++ .../test/integration/access.test.ts | 180 +++ .../test/integration/checkout.test.ts | 259 ++++ .../test/integration/products.test.ts | 136 ++ platforms/access-bridge/test/mock-server.ts | 48 + platforms/access-bridge/test/mocks/access.ts | 61 + .../access-bridge/test/mocks/checkout.ts | 73 + .../access-bridge/test/mocks/middleware.ts | 17 + platforms/access-bridge/test/mocks/payment.ts | 74 + .../access-bridge/test/mocks/products.ts | 30 + .../access-bridge/test/unit/errors.test.ts | 56 + .../access-bridge/test/unit/logger.test.ts | 98 ++ .../access-bridge/test/unit/signing.test.ts | 20 + platforms/access-bridge/test/vitest.setup.ts | 20 + platforms/access-bridge/tsconfig.json | 18 + platforms/access-bridge/vite.config.ts | 58 + yarn.lock | 1238 ++++++++++++++++- 53 files changed, 4402 insertions(+), 24 deletions(-) create mode 100644 packages/common/types/errors.ts create mode 100644 packages/common/types/passport.ts create mode 100644 packages/common/types/payment.ts create mode 100644 packages/common/types/plans.ts create mode 100644 platforms/access-bridge/.env.example create mode 100644 platforms/access-bridge/.eslintrc.cjs create mode 100644 platforms/access-bridge/.gitignore create mode 100644 platforms/access-bridge/.prettierignore create mode 100644 platforms/access-bridge/Makefile create mode 100644 platforms/access-bridge/README.md create mode 100644 platforms/access-bridge/docs/deployment.md create mode 100644 platforms/access-bridge/docs/developer-guidelines.md create mode 100644 platforms/access-bridge/docs/frameworks.md create mode 100644 platforms/access-bridge/lint-staged.config.cjs create mode 100644 platforms/access-bridge/package.json create mode 100644 platforms/access-bridge/prettier.config.cjs create mode 100644 platforms/access-bridge/src/app-config.ts create mode 100644 platforms/access-bridge/src/controllers/access-controller.ts create mode 100644 platforms/access-bridge/src/controllers/checkout-controller.ts create mode 100644 platforms/access-bridge/src/controllers/products-controller.ts create mode 100644 platforms/access-bridge/src/errors.ts create mode 100644 platforms/access-bridge/src/http.ts create mode 100644 platforms/access-bridge/src/main.ts create mode 100644 platforms/access-bridge/src/pipeline/logger.ts create mode 100644 platforms/access-bridge/src/pipeline/middleware.ts create mode 100644 platforms/access-bridge/src/pipeline/routes.ts create mode 100644 platforms/access-bridge/src/server.ts create mode 100644 platforms/access-bridge/src/services/identity-service.ts create mode 100644 platforms/access-bridge/src/services/passport-service.ts create mode 100644 platforms/access-bridge/src/services/payment-service.ts create mode 100644 platforms/access-bridge/src/services/plans-service.ts create mode 100644 platforms/access-bridge/src/services/stripe-payment-service.ts create mode 100644 platforms/access-bridge/stylelint.config.cjs create mode 100644 platforms/access-bridge/test/fixtures.ts create mode 100644 platforms/access-bridge/test/integration/access.test.ts create mode 100644 platforms/access-bridge/test/integration/checkout.test.ts create mode 100644 platforms/access-bridge/test/integration/products.test.ts create mode 100644 platforms/access-bridge/test/mock-server.ts create mode 100644 platforms/access-bridge/test/mocks/access.ts create mode 100644 platforms/access-bridge/test/mocks/checkout.ts create mode 100644 platforms/access-bridge/test/mocks/middleware.ts create mode 100644 platforms/access-bridge/test/mocks/payment.ts create mode 100644 platforms/access-bridge/test/mocks/products.ts create mode 100644 platforms/access-bridge/test/unit/errors.test.ts create mode 100644 platforms/access-bridge/test/unit/logger.test.ts create mode 100644 platforms/access-bridge/test/unit/signing.test.ts create mode 100644 platforms/access-bridge/test/vitest.setup.ts create mode 100644 platforms/access-bridge/tsconfig.json create mode 100644 platforms/access-bridge/vite.config.ts diff --git a/.github/workflows/test-unit-snapshot.yml b/.github/workflows/test-unit-snapshot.yml index 7f561c3b8..8d87ddbcf 100644 --- a/.github/workflows/test-unit-snapshot.yml +++ b/.github/workflows/test-unit-snapshot.yml @@ -1,4 +1,4 @@ -name: Test - Unit and Snapshot +name: Test - Unit, Integration, Snapshot on: push: @@ -21,3 +21,11 @@ jobs: yarn test env: CI: true + # Dummy environment variables for testing the access-bridge service + APP_SITE_ID: test1234 + APP_API_SECRET: dummy_secret + APP_STRIPE_SECRET: dummy_stripe_secret + APP_BIND_ADDR: localhost + APP_BIND_PORT: 8080 + APP_ACCESS_CONTROL_API_HOST: https://test-cdn.jwplayer.com + APP_SIMS_API_HOST: https://test-sims.jwplayer.com diff --git a/knip.config.ts b/knip.config.ts index 1d22ab1ee..e32756a97 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -38,7 +38,6 @@ const config: KnipConfig = { 'i18next-parser', 'luxon', // Used in tests 'playwright', // Used in test configs - 'sharp', // Requirement for @vite-pwa/assets-generator 'tsconfig-paths', // Used for e2e test setup 'virtual:pwa-register', // Service Worker code is injected at build time 'virtual:polyfills', // Polyfills are conditionally injected diff --git a/package.json b/package.json index c92bd4ca6..85821ed50 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "prepare": "husky install", "test": "TZ=UTC LC_ALL=en_US.UTF-8 vitest run", "test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest", - "web": "yarn --cwd platforms/web" + "web": "yarn --cwd platforms/web", + "access-bridge": "yarn --cwd platforms/access-bridge" }, "devDependencies": { "@commitlint/cli": "^17.8.1", diff --git a/packages/common/types/errors.ts b/packages/common/types/errors.ts new file mode 100644 index 000000000..c8d76f25a --- /dev/null +++ b/packages/common/types/errors.ts @@ -0,0 +1,8 @@ +export type JWError = { + code: string; + description: string; +}; + +export type JWErrorResponse = { + errors: JWError[]; +}; diff --git a/packages/common/types/passport.ts b/packages/common/types/passport.ts new file mode 100644 index 000000000..959197854 --- /dev/null +++ b/packages/common/types/passport.ts @@ -0,0 +1,4 @@ +export type PassportResponse = { + passport: string; + refresh_token: string; +}; diff --git a/packages/common/types/payment.ts b/packages/common/types/payment.ts new file mode 100644 index 000000000..dde4917aa --- /dev/null +++ b/packages/common/types/payment.ts @@ -0,0 +1,49 @@ +type Recurrence = { + // The frequency at which a subscription is billed. One of `day`, `week`, `month`, or `year`. + interval: 'day' | 'week' | 'month' | 'year'; + // Recurrence duration, for example, `interval=month` and `duration=3` bills every 3 months. + duration: number; + // Trial period interval. For example, a month-long trial is different from a 30-day trial. + trial_period_interval: 'day' | 'week' | 'month' | 'year'; + // Duration of the trial period (in the unit defined by trial_period_interval). + trial_period_duration: number | null; +}; + +export type Price = { + // Unique identifier for the object. + store_price_id: string; + // Dictionary of currencies, where the key is the currency code and the value is an object with an amount property. + // Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. + currencies: { + [currency_code: string]: { + // The unit amount in cents (or local equivalent) to be charged, represented as a whole integer. + amount: number | null; + }; + }; + // Default currency code for this price. + default_currency: string; + // Recurrence details. Can be a Recurrence object or 'one_time'. + recurrence: Recurrence | 'one_time'; + // Billing scheme. For now, we only support `per_unit`. + billing_scheme: 'per_unit'; +}; + +export type Product = { + // Unique identifier for the object. + store_product_id: string; + // The product's name, meant to be displayable to the customer. + name: string; + // The product's description, meant to be displayable to the customer. + description: string; + // The ID of the default price this product is associated with. + default_store_price_id: string; + // Array of price objects. + prices: Price[]; +}; + +// General checkout parameters type. Can be extended by specific payment providers, e.g. Stripe +export type CheckoutParams = { + price_id: string; + success_url: string; + cancel_url: string; +}; diff --git a/packages/common/types/plans.ts b/packages/common/types/plans.ts new file mode 100644 index 000000000..331bccfac --- /dev/null +++ b/packages/common/types/plans.ts @@ -0,0 +1,31 @@ +type AccessOptions = { + drm_policy_id: string; +}; + +type PlanExternalProviders = { + stripe?: string; + apple?: string; + google?: string; +}; + +export type AccessControlPlan = { + id: string; + exp: number; +}; + +export type Plan = { + id: string; + exp: number; + access_model: 'free' | 'freeauth' | 'svod'; + access: AccessOptions; + metadata: { + external_providers: PlanExternalProviders; + }; +}; + +export type PlansResponse = { + total: number; + page: number; + page_length: number; + plans: Plan[]; +}; diff --git a/platforms/access-bridge/.env.example b/platforms/access-bridge/.env.example new file mode 100644 index 000000000..82c77f2fc --- /dev/null +++ b/platforms/access-bridge/.env.example @@ -0,0 +1,37 @@ +# Application version - Used for tracking and identifying specific releases. +# Automatically set to the version specified in package.json. +# Helps in associating logs, errors, and other metrics with particular versions of the application. +APP_VERSION=$npm_package_version + +# Secrets responsible for signing a request to authenticate with the JW Delivery Gateway. +# In order to use the AC System this authentication is crucial. +# The secrets can be found in the JW Dashboard, under the API Credentials in the top right settings icon. +# Make sure the secrets are V1 and that they refer to the desired property. +# For production env, use this reference on how to store them: https://cloud.google.com/run/docs/configuring/services/secrets +APP_API_SECRET=customer_v1_secret +# site_id or property_id represents the key that corresponds to the APP_API_SECRET defined earlier. +APP_SITE_ID=customer_site_id +# Stripe secret responsible for authenticating Stripe API calls +APP_STRIPE_SECRET=stripe_secret + +# Non-secret variables +# Specifies the network address or IP address on which the server listens for incoming connections. +APP_BIND_ADDR=localhost +# Specifies the port number on which the server listens for incoming connections. +APP_BIND_PORT=8080 +# Specifies the client URL responsible for access related stuff +APP_ACCESS_CONTROL_API_HOST=https://cdn-dev.jwplayer.com +# Specifies the client URL responsible for plans related stuff +APP_SIMS_API_HOST=https://daily-sims.jwplayer.com + +# These are optional and should be added only if tracing with Sentry is needed +# Set the APP_SENTRY_DSN variable to enable Sentry error tracking and performance monitoring. +# Set APP_SENTRY_AUTH_TOKEN to allow Sentry to provide readable stack traces (source maps). +# If this variable is not set, Sentry will not be initialized. +# For production environments, ensure you configure the `APP_SENTRY_TRACE_RATE` +# according to your monitoring needs to balance performance and resource usage. +APP_SENTRY_DSN= +APP_SENTRY_AUTH_TOKEN= +APP_SENTRY_TRACE_RATE= +APP_SENTRY_ORG_NAME= +APP_SENTRY_PROJ_NAME= diff --git a/platforms/access-bridge/.eslintrc.cjs b/platforms/access-bridge/.eslintrc.cjs new file mode 100644 index 000000000..0eb7718e4 --- /dev/null +++ b/platforms/access-bridge/.eslintrc.cjs @@ -0,0 +1,12 @@ +module.exports = { + extends: ['jwp/typescript'], + rules: { + "max-len": ["error", { "code": 120 }], + "import/no-unused-modules": ["error"] + }, + env: { + node: true, // Enables recognition of Node.js global variables and scoping rules + }, + ignorePatterns: ['build'], +}; + diff --git a/platforms/access-bridge/.gitignore b/platforms/access-bridge/.gitignore new file mode 100644 index 000000000..bcea2a995 --- /dev/null +++ b/platforms/access-bridge/.gitignore @@ -0,0 +1,6 @@ +node_modules +build +*.local +.env +# Sentry Config File +.env.sentry-build-plugin diff --git a/platforms/access-bridge/.prettierignore b/platforms/access-bridge/.prettierignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/platforms/access-bridge/.prettierignore @@ -0,0 +1 @@ +build diff --git a/platforms/access-bridge/Makefile b/platforms/access-bridge/Makefile new file mode 100644 index 000000000..c22bdf66c --- /dev/null +++ b/platforms/access-bridge/Makefile @@ -0,0 +1,70 @@ +PACKAGE_JSON := ./package.json +BACKUP_PACKAGE_JSON := ./package-backup.json +ROOT_YARN_LOCK := ../../yarn.lock +PROJECT_YARN_LOCK := ./yarn.lock + +# Default target: deploy and ensure cleanup runs regardless +.PHONY: deploy +deploy: check-setup backup-package modify-package copy-yarn-lock try-deploy post-deploy + +# Step 0: Check setup +.PHONY: check-setup +check-setup: + @echo "Checking setup..." + @command -v gcloud >/dev/null 2>&1 || { echo "Error: gcloud is not installed. Please install Google Cloud SDK."; exit 1; } + @command -v yarn >/dev/null 2>&1 || { echo "Error: yarn is not installed. Please install Yarn."; exit 1; } + @echo "All required tools are installed." + @# Check if Google Cloud is configured + @gcloud config get-value project >/dev/null 2>&1 || { echo "Error: Google Cloud project is not configured. Run 'gcloud init'."; exit 1; } + @echo "Google Cloud is configured." + +# Step 1: Backup the original package.json +.PHONY: backup-package +backup-package: + @echo "Deployment started..." + @cp $(PACKAGE_JSON) $(BACKUP_PACKAGE_JSON) + +# Step 2: Modify `package.json` by removing internal dependencies +# In a monorepo setup, internal packages are not deployed. This step ensures that only the relevant, +# external dependencies are included in the `package.json` for deployment. +.PHONY: modify-package +modify-package: + @# Use `sed` to remove specific internal dependencies by editing package.json in place + @sed -i.bak '/"@jwp\/ott-common"/d' $(PACKAGE_JSON) + @sed -i.bak '/"eslint-config-jwp"/d' $(PACKAGE_JSON) + @rm -f $(PACKAGE_JSON).bak + +# Step 3: Copy the root `yarn.lock` to the project directory +# To maintain dependency consistency in a monorepo, copy the root `yarn.lock` file to the project +# directory so Google Cloud Run can correctly compare and resolve dependencies. +.PHONY: copy-yarn-lock +copy-yarn-lock: + @cp $(ROOT_YARN_LOCK) $(PROJECT_YARN_LOCK) + +# Step 4: Deploy to Google Cloud Run +.PHONY: try-deploy +try-deploy: + @{ \ + set -e; \ + trap 'make post-deploy; exit 1;' INT TERM HUP; \ + gcloud run deploy access-bridge \ + --source=. \ + --platform=managed \ + --region= \ + --allow-unauthenticated; \ + make post-deploy; \ + } + +# Step 5: Cleanup (run whether deploy succeeds or fails) +.PHONY: post-deploy +post-deploy: restore-package clean + +# Step 6: Restore the original package.json +.PHONY: restore-package +restore-package: + @mv $(BACKUP_PACKAGE_JSON) $(PACKAGE_JSON) + +# Step 7: Clean up copied yarn.lock +.PHONY: clean +clean: + @rm -f $(PROJECT_YARN_LOCK) diff --git a/platforms/access-bridge/README.md b/platforms/access-bridge/README.md new file mode 100644 index 000000000..946ba54f2 --- /dev/null +++ b/platforms/access-bridge/README.md @@ -0,0 +1,134 @@ +# Access Bridge + +A service that facilitates seamless communication between the Subscriber Identity Management System (SIMS) and Access Control services. It provides endpoints to generate access passports for authenticated viewers, ensuring secure and efficient access management. + +## Local Setup for Environment Variables + +To set up the project locally, you need to configure environment variables that are crucial for authenticating with the JW Delivery Gateway and for specifying server configurations. + +Here’s how you can set them up: + +Create a `.env.local` file in the root of this project and add the following variables: + +- APP_API_SITE_ID=customer_site_id +- APP_API_SECRET=customer_v1_secret +- APP_BIND_ADDR=localhost +- APP_BIND_PORT=8080 +- APP_ACCESS_CONTROL_API_HOST=https://cdn-dev.jwplayer.com + (Use https://cdn.jwplayer.com for production) +- APP_SIMS_API_HOST=https://daily-sims.jwplayer.com + (Use https://sims.jwplayer.com for production) + +Make sure to replace the placeholder values (e.g., customer_v1_secret) with the actual values from your JW Dashboard. +You can also copy and paste the contents of `.env.example` into `.env.local` and just adjust the APP_API_SECRET. + +## Getting started + +- Run `yarn` to install dependencies +- Navigate to the platform directory `cd platforms/access-bridge` +- Run tests through `yarn test` +- Run `yarn start` to start the server + +## Exposed endpoints + +#### URL: `/v2/sites/{site_id}/access/generate` + +- **Method:** PUT +- **Authorization:** Valid SIMS token +- **Summary:** Generates a new passport for an authenticated viewer based on the information inside the SIMS token. +- **Response:** + ```json + { + "passport": "encrypted_passport", + "refresh_token": "random_string" + } + ``` + +#### URL: `/v2/sites/{site_id}/access/refresh` + +- **Method:** PUT +- **Authorization:** Valid SIMS token +- **Summary:** Regenerates an existing passport with a new expiry and a new refresh token. +- **Request:** + ```json + { + "refresh_token": "string" + } + ``` +- **Response:** + ```json + { + "passport": "encrypted_passport", + "refresh_token": "random_string" + } + ``` + +#### URL: `/v2/sites/{site_id}/products` + +- **Method:** GET +- **Authorization:** None +- **Summary:** Lists all the corresponding stripe products with prices that are connected to the SIMS plans. +- **Response:** [Product payment type](../../../ott-web-app/packages/common/types/payment.ts) + ```json + [ + { + // ... + "id": "prod_QRUHbH7wK5HHPr", + "default_price": "price_1PabInA9TD3ZjIM6EEnKSR7U", + // ... + "prices": [ + { + // ... + "id": "price_1PabInA9TD3ZjIM6EEnKSR7U", + "currency": "usd", + "unit_amount": 15000 + // ... + } + ] + } + // ... + ] + ``` + +#### URL: `/v2/sites/{site_id}/checkout` + +- **Method:** POST +- **Authorization:** Valid SIMS token +- **Summary:** Creates Payment Checkout Session URL where the viewer will be redirected to complete the payment. +- **Request:** + ```json + { + "price_id": "string", // id of the price that is about to be paid + "mode": "string", // subscription (recurring) | payment (one time purchases) + "success_url": "string", // redirect after successful payment + "cancel_url": "string" // redirect after cancel / invalid payment + } + ``` +- **Response:** + ```json + { + "url": "string" // url where the viewer will be redirected to complete the payment. + } + ``` + +#### URL: `/v2/sites/{site_id}/billing-portal` + +- **Method:** POST +- **Authorization:** Valid SIMS token +- **Summary:** Generates Billing Portal Url, where the viewer can view / update their purchase info. +- **Response:** + ```json + { + "url": "billing-portal-url" + } + ``` + +## Developer guidelines + +- Read the workspace guidelines here [developer-guidelines.md](./docs/developer-guidelines.md). +- Read the deployment guidelines here [deployment.md](./docs/deployment.md). +- Read the web platform guidelines here [developer-guidelines.md](../../docs/developer-guidelines.md). + +``` + +``` diff --git a/platforms/access-bridge/docs/deployment.md b/platforms/access-bridge/docs/deployment.md new file mode 100644 index 000000000..cc0fab351 --- /dev/null +++ b/platforms/access-bridge/docs/deployment.md @@ -0,0 +1,50 @@ +# Deployment Instructions for `access-bridge` App + +This document outlines the steps required to deploy the `access-bridge` application to Google Cloud Run. The deployment process involves checking the setup, modifying `package.json`, copying the `yarn.lock` file, deploying the application, and performing cleanup tasks. + +## Prerequisites + +Ensure the following before deployment: + +- **Google Cloud SDK**: Install from [Google Cloud SDK Documentation](https://cloud.google.com/sdk/docs/install). +- Make sure to configure the Google Cloud project using `gcloud init`. + +## Environment Configuration + +Before deploying, ensure the environment variables and secrets required by the application are properly set up: + +1. **Environment Variables and Secrets**: + + - Review the `.env.example` file for a list of required environment variables. + - Set up these variables in Google Cloud Run. Secrets should be managed using [Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager), while non-sensitive variables can be set as environment variables in the Google Cloud Run settings. + + Ensure that all sensitive information is securely managed with Secret Manager to avoid exposure of secrets in your deployment. + +## Deployment Overview + +The `make deploy` command automates the deployment process. It performs the following tasks: + +1. **Check Setup**: Verifies that all necessary tools and configurations are in place. +2. **Backup `package.json`**: Creates a backup of the current `package.json` to restore later. +3. **Modify `package.json`**: Removes internal dependencies that may cause conflicts in Google Cloud Run. +4. **Copy `yarn.lock`**: Copies the root `yarn.lock` file to the project directory to ensure dependency consistency. +5. **Deploy**: Deploys the application to Google Cloud Run. +6. **Post-Deployment Cleanup**: Restores the original `package.json` and removes the copied `yarn.lock` file. + +## Deployment Steps + +Before running the deployment command, ensure the following: + +1. **Clean Working Directory**: Ensure there are no uncommitted changes in your working directory. The deployment process assumes a clean state. + +2. **Install Dependencies**: Run `yarn install` to ensure all dependencies are installed and up-to-date. + +3. **Navigate to Project Directory**: Make sure you are in the `access-bridge` directory before running the deployment command. + +4. **Configure Deployment Region**: When running the deployment command, you will be prompted to enter the region where you want to deploy the application. If you prefer not to be prompted each time, you can modify the `Makefile` to include the `--region` flag directly with your preferred region. + +To deploy the `access-bridge` application, run the following command: + +```sh +make deploy +``` diff --git a/platforms/access-bridge/docs/developer-guidelines.md b/platforms/access-bridge/docs/developer-guidelines.md new file mode 100644 index 000000000..cc6239581 --- /dev/null +++ b/platforms/access-bridge/docs/developer-guidelines.md @@ -0,0 +1,81 @@ +# Developer guidelines + +## When working on this project, keep these in mind: + +- Use `yarn` to install dependencies +- Run tests through `yarn test` +- Start the project using `yarn start` + +## Project Structure + +``` +/build* - Directory where the code is compiled by `yarn build` +/docs - Documentation related to the project +/node_modules* - Yarn generated dependencies +/src - Source code for the application + /controllers - Controller modules containing the core logic for handling requests and responses + /services - Services which connect external data sources to the application + /pipeline - Middleware and routing logic + /middleware.ts - Middleware functions and error handling + /routes.ts - Route definitions and route registration + /logger.ts - Logger class that integrates with Sentry if defined with fallback as a development logger + /errors.ts - Custom error classes and error handling logic + /http.ts - HTTP utility functions and setup + /app-config.ts - Configuration settings for the application + /main.ts - Main entry point of the application + /server.ts - Server initialization and configuration +/test - Data and scripts for testing +/.env<.mode> - Environment variables for different modes (e.g., development, production) +/package.json - Yarn file for dependencies and scripts + + + + +* = Generated directories, not in source control + +Note: Some system and util files are not shown above for brevity. +You probably won't need to mess with anything not shown here. +``` + +## Sentry Setup and Configuration + +### Integrating Sentry + +Sentry is integrated into this project to track and monitor errors, as well as performance issues. To ensure that Sentry is properly configured and working as expected, follow the steps below: + +- Environment Variables: + Ensure that your .env. files include the following Sentry-related environment variables: + APP_SENTRY_DSN=Your_Sentry_DSN + APP_SENTRY_AUTH_TOKEN=Your_Sentry_Auth_Token + APP_SENTRY_TRACE_RATE=Trace rate for performance monitoring + APP_SENTRY_ORG_NAME=Your_Sentry_ORG_Name + APP_SENTRY_PROJ_NAME=Your_Sentry_Project_Name + These variables are essential for Sentry to function correctly and should be kept secure. + +- Running the Project: + Use the following commands to install dependencies, run tests, and start the project, with Sentry automatically configured: + Install dependencies: yarn + Run tests: yarn test + Start the project: yarn start + +- Source Maps: + Source maps are configured to be generated during the build process to allow Sentry to provide readable stack traces. + When building the project, the source maps will be automatically uploaded to Sentry if the correct environment variables are set. + +- Error Monitoring: + Sentry captures unhandled errors and sends detailed reports, including stack traces, to help in debugging. + Review the Sentry dashboard regularly to monitor the health of the application. + +- Performance Monitoring: + Sentry is also configured for performance monitoring. Ensure that the trace rate is appropriately set in the environment variables. + +## Adding Sentry to New Features + +When adding new features or making significant changes to the code: + +- Error Handling: + Ensure that errors are properly captured and logged using Sentry. +- Custom Error Classes: + If you define custom error classes, integrate them with Sentry for better tracking. +- Performance Considerations: + For critical paths, consider adding custom performance monitoring using Sentry’s APIs. diff --git a/platforms/access-bridge/docs/frameworks.md b/platforms/access-bridge/docs/frameworks.md new file mode 100644 index 000000000..219ae0da6 --- /dev/null +++ b/platforms/access-bridge/docs/frameworks.md @@ -0,0 +1,76 @@ +# Language, Frameworks, SDK's and Libraries + +## Typescript + +Typescript is a superset of Javascript, TypeScript adds optional types to JavaScript that support tools for large-scale JavaScript applications for any browser, for any host, on any OS. TypeScript compiles to readable, standards-based JavaScript. + +- Optional static typing +- Spot bugs at compile time +- Predictability +- Readability +- Fast refactoring +- Power of OOP + +[Read more.](https://www.typescriptlang.org/) + +## Express + +Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It's designed to build web applications and APIs, offering a thin layer of fundamental web application features without obscuring Node.js's built-in features. + +- Minimal and flexible +- Powerful routing and middleware support +- Full HTTP utility methods and middleware +- Fast and unopinionated +- Ideal for building APIs and single-page applications + +[Read more.](https://expressjs.com/) + +## Vitest + +Vitest is a blazing-fast unit testing framework built for modern JavaScript and TypeScript projects. It is designed to work seamlessly with Vite, leveraging its speed and simplicity, but can be used in any project. Vitest provides an intuitive and powerful API that makes writing tests easy and efficient. + +- Fast test execution with minimal configuration +- Built-in TypeScript support +- Instant feedback with hot reloading +- Integrated with Vite but framework-agnostic +- Snapshot testing, mocking, and more + +[Read more.](https://vitest.dev/) + +## JSON Web Token (jsonwebtoken) + +The jsonwebtoken library is used for generating and verifying JSON Web Tokens (JWTs). JWTs are a compact and secure way to transmit information between parties as a JSON object. This library is particularly useful for authentication and authorization purposes. + +- Generate and verify JWTs +- Compact and URL-safe tokens +- Supports various signing algorithms +- Easy integration with existing systems +- Used for secure API calls through the Delivery Gateway + +[Read more.](https://github.com/auth0/node-jsonwebtoken/) + +## Stripe + +The Stripe SDK provides a comprehensive set of APIs and tools for integrating payment processing into web and mobile applications. With the Stripe SDK, developers can easily manage transactions, subscriptions, and financial reports, while ensuring secure and compliant payment handling. + +- Seamless payment integration +- Support for multiple payment methods +- Advanced fraud prevention +- Subscription and billing management +- Detailed financial reporting +- PCI compliance and secure transactions + +[Read more.](https://docs.stripe.com/) + +## Sentry + +Sentry is an open-source error tracking and performance monitoring tool that helps developers monitor and fix crashes in real-time. With Sentry, you can identify and address performance bottlenecks, errors, and exceptions in your applications. Sentry provides detailed stack traces, allowing developers to see exactly where an issue occurred, and integrates seamlessly with many popular languages and frameworks. + +- Real-time error tracking and alerting +- Performance monitoring and tracing +- Detailed stack traces for easy debugging +- Seamless integration with various languages and frameworks +- Support for source maps to map minified code to original source code +- Customizable notifications and workflow integrations + +[Read more.](https://docs.sentry.io/) diff --git a/platforms/access-bridge/lint-staged.config.cjs b/platforms/access-bridge/lint-staged.config.cjs new file mode 100644 index 000000000..998018c6a --- /dev/null +++ b/platforms/access-bridge/lint-staged.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + // Lint and format JavaScript and TypeScript files + '*.{js,ts}': ['eslint --fix', 'prettier --write'], + + // Type check TypeScript files + '*.ts': [() => 'tsc --pretty --noEmit'], +}; diff --git a/platforms/access-bridge/package.json b/platforms/access-bridge/package.json new file mode 100644 index 000000000..64b19c00e --- /dev/null +++ b/platforms/access-bridge/package.json @@ -0,0 +1,42 @@ +{ + "name": "@jwp/access-bridge", + "version": "1.0.0", + "main": "build/main.js", + "engines": { + "node": ">=18.13.0" + }, + "private": true, + "author": "JW Player", + "type": "module", + "scripts": { + "build": "vite build --mode ${MODE:=prod}", + "build-test": "vite build --mode test", + "start": "node build/main.js", + "prestart": "yarn build", + "pretest": "yarn build-test", + "test": "TZ=UTC LC_ALL=en_US.UTF-8 vitest run", + "lint:ts": "tsc --pretty --noEmit -p ./" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.14.10", + "eslint-config-jwp": "*", + "typescript": "^5.5.3", + "vite": "^5.3.1", + "vite-plugin-node": "^3.1.0", + "vitest": "^1.6.0" + }, + "dependencies": { + "@jwp/ott-common": "*", + "@sentry/node": "^8.26.0", + "@sentry/profiling-node": "^8.26.0", + "@sentry/vite-plugin": "^2.22.2", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "stripe": "^16.8.0" + } +} diff --git a/platforms/access-bridge/prettier.config.cjs b/platforms/access-bridge/prettier.config.cjs new file mode 100644 index 000000000..5db66ae34 --- /dev/null +++ b/platforms/access-bridge/prettier.config.cjs @@ -0,0 +1,11 @@ +module.exports = { + printWidth: 120, // Set the maximum line length + tabWidth: 2, // Number of spaces per indentation level + useTabs: false, // Use spaces for indentation instead of tabs + singleQuote: true, // Use single quotes instead of double quotes + trailingComma: 'es5', // Add trailing commas where valid in ES5 (objects, arrays) + bracketSpacing: true, // Print spaces between brackets in object literals + semi: true, // Print semicolons at the ends of statements + arrowParens: 'always', // Include parentheses around a sole arrow function parameter + endOfLine: 'lf', // Enforce Unix line endings +}; diff --git a/platforms/access-bridge/src/app-config.ts b/platforms/access-bridge/src/app-config.ts new file mode 100644 index 000000000..e24a92ba4 --- /dev/null +++ b/platforms/access-bridge/src/app-config.ts @@ -0,0 +1,39 @@ +import dotenv from 'dotenv'; +// Inject environment variables at runtime +dotenv.config(); + +// Function to assert an environment variable is defined +function requireEnvVar(env: string | undefined, name: string): string { + if (!env) { + throw new Error(`Environment variable "${name}" is not defined.`); + } + return env; +} + +// Secrets resonsible for authenticating requests +export const API_SECRET = requireEnvVar(process.env.APP_API_SECRET, 'APP_API_SECRET'); +export const STRIPE_SECRET = requireEnvVar(process.env.APP_STRIPE_SECRET, 'APP_STRIPE_SECRET'); + +// SITE_ID specifies the property that the customer is using +export const SITE_ID = requireEnvVar(process.env.APP_SITE_ID, 'APP_SITE_ID'); + +// BIND_ADDR specifies the network address or IP address on which the server listens for incoming connections. +// This could be an IP address (e.g., '127.0.0.1' for localhost) or a hostname. +export const BIND_ADDR = requireEnvVar(process.env.APP_BIND_ADDR, 'APP_BIND_ADDR'); + +// BIND_PORT specifies the port number on which the server listens for incoming connections. +// Ensure this port is available and not in use by another application. +export const BIND_PORT = (() => { + const port = parseInt(requireEnvVar(process.env.APP_BIND_PORT, 'APP_BIND_PORT'), 10); + if (isNaN(port)) { + throw new Error(`Environment variable "APP_BIND_PORT" must be a valid number.`); + } + return port; +})(); + +// Client host URLs +export const ACCESS_CONTROL_API_HOST = requireEnvVar( + process.env.APP_ACCESS_CONTROL_API_HOST, + 'APP_ACCESS_CONTROL_API_HOST' +); +export const SIMS_API_HOST = requireEnvVar(process.env.APP_SIMS_API_HOST, 'APP_SIMS_API_HOST'); diff --git a/platforms/access-bridge/src/controllers/access-controller.ts b/platforms/access-bridge/src/controllers/access-controller.ts new file mode 100644 index 000000000..e6f6208b4 --- /dev/null +++ b/platforms/access-bridge/src/controllers/access-controller.ts @@ -0,0 +1,63 @@ +import { Request, Response, NextFunction } from 'express'; + +import { ErrorDefinitions, sendErrors } from '../errors.js'; +import { PassportService } from '../services/passport-service.js'; +import { PlansService } from '../services/plans-service.js'; +import { IdentityService, Viewer } from '../services/identity-service.js'; + +/** + * Controller class responsible for handling access-related services. + * The controller interacts with services for identity management, plans management, and passport generation. + */ +export class AccessController { + private readonly identityService: IdentityService; + private readonly passportService: PassportService; + private readonly plansService: PlansService; + + constructor() { + this.identityService = new IdentityService(); + this.passportService = new PassportService(); + this.plansService = new PlansService(); + } + + /** + * Service handler for generating passport access tokens based on the provided authorization token. + * Retrieves access control plans and generates access tokens. + */ + async generatePassport(req: Request, res: Response, next: NextFunction): Promise { + // Anonymous is default for not authenticated viewers. + // These viewers only have access to free plans. + let viewer: Viewer = { id: 'anonymous', email: '' }; + + const authorization = req.headers['authorization']; + if (authorization) { + viewer = await this.identityService.getAccount({ authorization }); + if (!viewer.id || !viewer.email) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + } + + const viewerEntitledPlans = await this.plansService.getEntitledPlans({ authorization }); + const plans = viewerEntitledPlans + .map((plan) => ({ + id: plan.id, + exp: plan?.exp, + })) + .filter((plan) => plan.id !== undefined && plan.exp !== undefined); + + const passport = await this.passportService.generatePassport({ viewerId: viewer.id, plans }); + + res.json(passport); + } + + /** + * Service handler for refreshing access tokens based on the provided refresh token. + */ + async refreshPassport(req: Request, res: Response, next: NextFunction): Promise { + const { refresh_token: refreshToken } = req.body; + const passport = await this.passportService.refreshPassport({ refreshToken }); + + res.json(passport); + } +} diff --git a/platforms/access-bridge/src/controllers/checkout-controller.ts b/platforms/access-bridge/src/controllers/checkout-controller.ts new file mode 100644 index 000000000..5610000ba --- /dev/null +++ b/platforms/access-bridge/src/controllers/checkout-controller.ts @@ -0,0 +1,71 @@ +import { Request, Response, NextFunction } from 'express'; + +import { ErrorDefinitions, sendErrors } from '../errors.js'; +import { IdentityService } from '../services/identity-service.js'; +import { PaymentService } from '../services/payment-service.js'; +import { StripePaymentService } from '../services/stripe-payment-service.js'; + +/** + * Controller class responsible for handling payment checkout session URLs, where the viewers can complete the payment. + */ +export class CheckoutController { + private readonly identityService: IdentityService; + private readonly paymentService: PaymentService; + + constructor() { + this.identityService = new IdentityService(); + this.paymentService = new StripePaymentService(); + } + + /** + * Service handler for initiating a Payment Checkout session based on the provided checkout params. + * @returns A Promise that resolves with a response containing the URL for the Payment Provider Checkout session. + */ + async initiateCheckout(req: Request, res: Response, next: NextFunction): Promise { + const authorization = req.headers['authorization']; + if (!authorization) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + + const checkoutParams = req.body; + const validationError = this.paymentService.validateCheckoutParams(checkoutParams); + if (validationError) { + sendErrors(res, ErrorDefinitions.ParameterMissingError.create({ parameterName: validationError })); + return; + } + + const viewer = await this.identityService.getAccount({ authorization }); + const checkoutSessionUrl = await this.paymentService.createCheckoutSessionUrl(viewer, checkoutParams); + + res.json({ url: checkoutSessionUrl }); + } + + /** + * Service handler for generating a Billing portal session URL based on the provided viewer. + * Viewers are redirected to this URL where they can manage their purchase info. + * @returns A Promise that resolves with a response containing the URL for the Billing Portal session. + */ + async generateBillingPortalURL(req: Request, res: Response, next: NextFunction): Promise { + const authorization = req.headers['authorization']; + if (!authorization) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + + const viewer = await this.identityService.getAccount({ authorization }); + if (!viewer.id || !viewer.email) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + + const { return_url } = req.body; + if (!return_url) { + sendErrors(res, ErrorDefinitions.ParameterMissingError.create({ parameterName: 'return_url' })); + return; + } + + const billingPortalSessionUrl = await this.paymentService.createBillingPortalSessionUrl(viewer, return_url); + res.json({ url: billingPortalSessionUrl }); + } +} diff --git a/platforms/access-bridge/src/controllers/products-controller.ts b/platforms/access-bridge/src/controllers/products-controller.ts new file mode 100644 index 000000000..dc48d6c3e --- /dev/null +++ b/platforms/access-bridge/src/controllers/products-controller.ts @@ -0,0 +1,32 @@ +import { Request, Response, NextFunction } from 'express'; + +import { PlansService } from '../services/plans-service.js'; +import { StripePaymentService } from '../services/stripe-payment-service.js'; +import { PaymentService } from '../services/payment-service.js'; +/** + * Controller class responsible for handling AC plans and Stripe products. + */ +export class ProductsController { + private readonly plansService: PlansService; + private readonly paymentService: PaymentService; + + constructor() { + this.plansService = new PlansService(); + this.paymentService = new StripePaymentService(); + } + + /** + * Service handler for fetching and returning products from the used Provider with prices based on available plans. + * Retrieves and filters SIMS plans, then matches them with the Payment products based on the external provider IDs. + */ + async getProducts(req: Request, res: Response, next: NextFunction): Promise { + const availablePlans = await this.plansService.getAvailablePlans(); + const stripeProductIds: string[] = availablePlans + // Currently, we only support Stripe as a payment provider, so we filter and use only Stripe product IDs. + .map((plan) => plan.metadata.external_providers?.stripe ?? []) + .flat(); + + const products = await this.paymentService.getProductsWithPrices(stripeProductIds); + res.json(products); + } +} diff --git a/platforms/access-bridge/src/errors.ts b/platforms/access-bridge/src/errors.ts new file mode 100644 index 000000000..0de8c886f --- /dev/null +++ b/platforms/access-bridge/src/errors.ts @@ -0,0 +1,176 @@ +import Stripe from 'stripe'; +import { Response } from 'express'; +import { JWError, JWErrorResponse } from '@jwp/ott-common/types/errors.js'; + +// Define types for error codes and status codes +export type ErrorCode = keyof typeof ErrorDefinitions; +export type ErrorStatusCode = (typeof ErrorDefinitions)[ErrorCode]['statusCode']; + +// Define context types for each error +interface BaseContext { + description?: string; +} + +interface ParameterMissingContext extends BaseContext { + parameterName: string; +} + +interface ParameterInvalidContext extends BaseContext { + parameterName: string; + reason?: string; +} + +// Unified Error Definitions with details and creation functions +export const ErrorDefinitions = { + BadRequestError: { + code: 'bad_request', + statusCode: 400, + description: 'The request was not constructed correctly.', + create: (context?: BaseContext) => new AccessBridgeError('BadRequestError', context), + }, + ParameterMissingError: { + code: 'parameter_missing', + statusCode: 400, + description: 'Required parameter is missing.', + create: (context: Partial) => { + const description = context.description + ? context.description + : context.parameterName + ? `Required parameter ${context.parameterName} is missing.` + : 'Required parameter is missing.'; + + return new AccessBridgeError('ParameterMissingError', { + ...context, + description, + }); + }, + }, + ParameterInvalidError: { + code: 'parameter_invalid', + statusCode: 400, + description: 'Parameter is invalid.', + create: (context: Partial) => { + const parameterName = context.parameterName ? `Parameter ${context.parameterName}` : 'Parameter'; + const reason = context.reason ? ` ${context.reason}` : ''; + const description = `${parameterName} is invalid.${reason}`; + + return new AccessBridgeError('ParameterInvalidError', { + ...context, + description, + }); + }, + }, + UnauthorizedError: { + code: 'unauthorized', + statusCode: 401, + description: 'Missing or invalid auth credentials.', + create: (context?: BaseContext) => new AccessBridgeError('UnauthorizedError', context), + }, + ForbiddenError: { + code: 'forbidden', + statusCode: 403, + description: 'Access to the requested resource is not allowed.', + create: (context?: BaseContext) => new AccessBridgeError('ForbiddenError', context), + }, + NotFoundError: { + code: 'not_found', + statusCode: 404, + description: 'The requested resource could not be found.', + create: (context?: BaseContext) => new AccessBridgeError('NotFoundError', context), + }, + MethodNotAllowedError: { + code: 'method_not_allowed', + statusCode: 405, + description: 'The used HTTP method is not supported on the given resource.', + create: (context?: BaseContext) => new AccessBridgeError('MethodNotAllowedError', context), + }, + InternalError: { + code: 'internal_error', + statusCode: 500, + description: 'An error was encountered while processing the request. Please try again.', + create: (context?: BaseContext) => new AccessBridgeError('InternalError', context), + }, +} as const; + +// Create the base class for errors +export class AccessBridgeError extends Error { + private readonly errorKey: keyof typeof ErrorDefinitions; + private readonly context?: BaseContext; + + constructor(errorKey: keyof typeof ErrorDefinitions, context?: BaseContext) { + super(); + this.errorKey = errorKey; + this.context = context; + } + + get code(): string { + return ErrorDefinitions[this.errorKey].code; + } + + get statusCode(): ErrorStatusCode { + return ErrorDefinitions[this.errorKey].statusCode; + } + + protected get defaultDescription(): string { + return ErrorDefinitions[this.errorKey].description; + } + + get description(): string { + return this.context?.description || this.defaultDescription; + } +} + +// Send errors +export function sendErrors(res: Response, error: AccessBridgeError): void { + const statusCode = error.statusCode; + res.status(statusCode).json({ + errors: [ + { + code: error.code, + description: error.description, + }, + ], + }); +} + +// Type guard to check if the error is a JWErrorResponse +export function isJWError(error: unknown): error is JWErrorResponse { + return ( + typeof error === 'object' && + error !== null && + 'errors' in error && + Array.isArray((error as { errors: unknown }).errors) && + (error as { errors: unknown[] }).errors.every( + (e) => typeof (e as JWError).code === 'string' && typeof (e as JWError).description === 'string' + ) + ); +} + +// Utility function to handle JW errors +export function handleJWError(error: JWErrorResponse): AccessBridgeError { + const jwError = error.errors[0]; + const { code, description } = jwError; + const errorDefinition = Object.keys(ErrorDefinitions).find( + (key) => ErrorDefinitions[key as keyof typeof ErrorDefinitions].code === code + ); + if (errorDefinition) { + return ErrorDefinitions[errorDefinition as keyof typeof ErrorDefinitions].create({ description }); + } + + // Fallback to a generic BadRequestError if no specific match is found + return ErrorDefinitions.BadRequestError.create({ description }); +} + +// Utility function to handle Stripe errors +export function handleStripeError(error: Stripe.errors.StripeError): AccessBridgeError { + if (error.type === 'StripeInvalidRequestError') { + throw ErrorDefinitions.BadRequestError.create({ description: error.message }); + } else if (error.type === 'StripeAuthenticationError') { + throw ErrorDefinitions.UnauthorizedError.create({ description: error.message }); + } else if (error.type === 'StripePermissionError') { + throw ErrorDefinitions.ForbiddenError.create({ description: error.message }); + } + + // Fallback to a generic BadRequestError for unexpected Stripe errors + throw ErrorDefinitions.BadRequestError.create({ description: error.message }); +} diff --git a/platforms/access-bridge/src/http.ts b/platforms/access-bridge/src/http.ts new file mode 100644 index 000000000..a1496e84f --- /dev/null +++ b/platforms/access-bridge/src/http.ts @@ -0,0 +1,91 @@ +/** + * Performs a GET request using fetch with optional authentication token. + * @param url The URL to fetch. + * @param token Optional authentication token for authorization header. + * @returns A promise that resolves to the parsed JSON response. + */ +export async function get(url: string, token?: string): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const response = await fetch(url, { + method: 'GET', + headers, + }); + + return await handleResponse(response); +} + +/** + * Performs a POST request using fetch with JSON body and optional authentication token. + * @param url The URL to fetch. + * @param body The JSON body to send in the POST request. + * @param token Optional authentication token for authorization header. + * @returns A promise that resolves to the parsed JSON response. + */ +export async function post(url: string, body: U, token?: string): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + + return await handleResponse(response); +} + +/** + * Performs a PUT request using fetch with JSON body and optional authentication token. + * @param url The URL to fetch. + * @param body The JSON body to send in the PUT request. + * @param token Optional authentication token for authorization header. + * @returns A promise that resolves to the parsed JSON response. + */ +export async function put(url: string, body: U, token?: string): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers['Authorization'] = token; + } + + const response = await fetch(url, { + method: 'PUT', + headers, + body: JSON.stringify(body), + }); + + return await handleResponse(response); +} + +// Handles the response from a fetch request, throwing an error if the response is not ok. +async function handleResponse(response: Response): Promise { + if (!response.ok) { + let errorData; + try { + errorData = await response.json(); + } catch (error) { + // If parsing JSON fails, throw with status text + throw new Error(`${response.status}, ${response.statusText}`); + } + + // Throw the original error data as-is + throw errorData; + } + + // If response is ok, return the parsed JSON content + return response.json(); +} diff --git a/platforms/access-bridge/src/main.ts b/platforms/access-bridge/src/main.ts new file mode 100644 index 000000000..36a037760 --- /dev/null +++ b/platforms/access-bridge/src/main.ts @@ -0,0 +1,47 @@ +import { BIND_ADDR, BIND_PORT } from './app-config.js'; +import { initializeRoutes } from './pipeline/routes.js'; +import { Server } from './server.js'; +import logger from './pipeline/logger.js'; + +if (BIND_PORT <= 0 || BIND_PORT > 65535) { + logger.error('Error: BIND_PORT must be a valid port number between 1 and 65535.'); + process.exit(1); +} + +const server = new Server(BIND_ADDR, BIND_PORT, initializeRoutes); + +async function startServer() { + try { + await server.listen(); + } catch (err) { + logger.error('Failed to start server:', err); + process.exit(1); + } +} + +startServer(); + +// Gracefully handle termination signals (e.g., Ctrl+C) +process.on('SIGINT', async () => { + logger.info('Shutting down server gracefully..'); + try { + await server.close(); + logger.info('Server closed.'); + process.exit(0); + } catch (err) { + logger.error('Error closing server:', err); + process.exit(1); + } +}); + +// Handle uncaught exceptions +process.on('uncaughtException', (err) => { + logger.error('Uncaught exception occurred:', err); + process.exit(1); +}); + +// Handle unhandled promise rejections +process.on('unhandledRejection', (reason, promise) => { + logger.error(`Unhandled Rejection at: ${promise}:`, reason); + process.exit(1); +}); diff --git a/platforms/access-bridge/src/pipeline/logger.ts b/platforms/access-bridge/src/pipeline/logger.ts new file mode 100644 index 000000000..4f6dabae9 --- /dev/null +++ b/platforms/access-bridge/src/pipeline/logger.ts @@ -0,0 +1,79 @@ +import * as Sentry from '@sentry/node'; +import { nodeProfilingIntegration } from '@sentry/profiling-node'; + +/** + * Logger class for handling various log levels and sending error reports to Sentry if integrated. + */ +class Logger { + constructor() { + this.initializeSentry(); + } + + /** + * Configures the Sentry client for error and performance monitoring. + * Reads Sentry configuration from environment variables and initializes Sentry if a DSN is provided. + */ + private initializeSentry() { + if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + return; // Skip Sentry initialization for non-production/test environments + } + + const SENTRY_DSN = process.env.APP_SENTRY_DSN || ''; + const SENTRY_TRACE_RATE = parseFloat(process.env.APP_SENTRY_TRACE_RATE || '1.0'); + + if (SENTRY_DSN) { + Sentry.init({ + dsn: SENTRY_DSN, + integrations: [nodeProfilingIntegration()], + environment: process.env.MODE || 'development', + release: process.env.APP_VERSION, + tracesSampleRate: SENTRY_TRACE_RATE, + profilesSampleRate: SENTRY_TRACE_RATE, + }); + } + } + + /** + * Logs an error message and sends it to Sentry if integrated. + * @param message - The error message to log. + * @param error - An optional error object to capture. + */ + error(message: string, error?: unknown) { + console.error(message, error ?? ''); + + if (Sentry.getClient()) { + if (error) { + Sentry.captureException(error); + } else { + Sentry.captureMessage(message, 'error'); + } + } + } + + /** + * Logs an informational message and sends it to Sentry if integrated. + * @param message - The informational message to log. + */ + info(message: string) { + console.info(message); + + if (Sentry.getClient()) { + Sentry.captureMessage(message, 'info'); + } + } + + /** + * Logs a warning message and sends it to Sentry if integrated. + * @param message - The warning message to log. + */ + warn(message: string) { + console.warn(message); + + if (Sentry.getClient()) { + Sentry.captureMessage(message, 'warning'); + } + } +} + +const logger = new Logger(); +export default logger; diff --git a/platforms/access-bridge/src/pipeline/middleware.ts b/platforms/access-bridge/src/pipeline/middleware.ts new file mode 100644 index 000000000..74615547d --- /dev/null +++ b/platforms/access-bridge/src/pipeline/middleware.ts @@ -0,0 +1,113 @@ +import * as Sentry from '@sentry/node'; +import Stripe from 'stripe'; +import express, { Request, Response, NextFunction, Express } from 'express'; +import cors from 'cors'; + +import { + AccessBridgeError, + ErrorDefinitions, + handleJWError, + handleStripeError, + isJWError, + sendErrors, +} from '../errors.js'; +import { SITE_ID } from '../app-config.js'; + +import logger from './logger.js'; + +/** + * Middleware class encapsulates global and route-specific middleware. + */ +export class Middleware { + /** + * This ensures that any errors in async operations are caught and passed to the global error handler + */ + asyncWrapper(fn: (req: Request, res: Response, next: NextFunction) => Promise) { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; + } + + /** + * Middleware to validate 'site_id' parameter. + * This should be applied to routes that require site_id validation. + */ + validateSiteId = (req: Request, res: Response, next: NextFunction) => { + if (req.params.site_id !== SITE_ID) { + sendErrors(res, ErrorDefinitions.ParameterInvalidError.create({ parameterName: 'site_id' })); + return; + } + next(); + }; + + /** + * Global error handler middleware for the server. + * This handles AccessBridge-specific errors and other unexpected errors. + */ + globalErrorHandler = (err: unknown, req: Request, res: Response, next: NextFunction) => { + // Handles SyntaxError in request body by responding with a ParameterInvalidError. + if (err instanceof SyntaxError && 'body' in err) { + sendErrors(res, ErrorDefinitions.ParameterInvalidError.create({ parameterName: 'body' })); + return; + } + + if (err instanceof AccessBridgeError) { + sendErrors(res, err); + return; + } + + if (err instanceof Stripe.errors.StripeError) { + const accessBridgeError = handleStripeError(err); + sendErrors(res, accessBridgeError); + } + + if (isJWError(err)) { + const accessBridgeError = handleJWError(err); + sendErrors(res, accessBridgeError); + return; + } + + logger.error('Unexpected error:', err); + sendErrors(res, ErrorDefinitions.InternalError.create()); + }; + + /** + * Middleware to handle 404 Not Found errors. + * This should be registered after all routes to handle undefined endpoints. + */ + notFoundErrorHandler = (req: Request, res: Response, next: NextFunction) => { + sendErrors(res, ErrorDefinitions.NotFoundError.create()); + }; + + /** + * Registers global middlewares. + * @param app The Express application. + */ + initializeCoreMiddleware(app: Express) { + // Middleware to enable Cross-Origin Resource Sharing (CORS) + app.use(cors()); + // Middleware for parsing JSON request bodies + app.use(express.json()); + } + + /** + * Registers global error handling middleware. + * This should be called after all routes are defined to catch errors and handle 404 responses. + * @param app The Express application. + */ + initializeErrorMiddleware(app: Express) { + app.use(this.notFoundErrorHandler); // Handle 404 errors + app.use(this.globalErrorHandler); // Handle all other errors + } + + /** + * Registers Sentry error handler for Expresss. + * The error handler must be registered before any other error middleware and after all controllers + * @param app The Express application. + */ + initializeSentryMiddleware(app: Express) { + if (Sentry.getClient()) { + Sentry.setupExpressErrorHandler(app); + } + } +} diff --git a/platforms/access-bridge/src/pipeline/routes.ts b/platforms/access-bridge/src/pipeline/routes.ts new file mode 100644 index 000000000..db4c2abbe --- /dev/null +++ b/platforms/access-bridge/src/pipeline/routes.ts @@ -0,0 +1,37 @@ +import { Express, Request, Response, NextFunction } from 'express'; + +import { AccessController } from '../controllers/access-controller.js'; +import { ProductsController } from '../controllers/products-controller.js'; +import { CheckoutController } from '../controllers/checkout-controller.js'; + +import { Middleware } from './middleware.js'; + +const middleware = new Middleware(); +const accessController = new AccessController(); +const productsController = new ProductsController(); +const checkoutController = new CheckoutController(); + +/* Disable ESLint max-len and Prettier formatting to keep routes on one line for better readability. */ +/* eslint-disable max-len */ +// prettier-ignore +export function initializeRoutes(app: Express) { + // Register routes with their respective controller methods + addRoute(app, 'put', '/v2/sites/:site_id/access/generate', accessController.generatePassport.bind(accessController)); + addRoute(app, 'put', '/v2/sites/:site_id/access/refresh', accessController.refreshPassport.bind(accessController)); + addRoute(app, 'get', '/v2/sites/:site_id/products', productsController.getProducts.bind(productsController)); + addRoute(app, 'post', '/v2/sites/:site_id/checkout', checkoutController.initiateCheckout.bind(checkoutController)); + addRoute(app, 'post', '/v2/sites/:site_id/billing-portal', checkoutController.generateBillingPortalURL.bind(checkoutController)); +} + +// Adds a route to the Express application with the specified HTTP method, path, and handler. +export function addRoute( + app: Express, + method: 'get' | 'post' | 'put', + path: string, + handler: (req: Request, res: Response, next: NextFunction) => Promise, + customMiddleware: Array<(req: Request, res: Response, next: NextFunction) => void> = [middleware.validateSiteId] +) { + // By default, validateSiteId middleware is added on each registered route with possibility to override. + // asyncWrapper ensures that any errors in async operations are caught and passed to the global error handler. + app[method](path, [...customMiddleware, middleware.asyncWrapper(handler)]); +} diff --git a/platforms/access-bridge/src/server.ts b/platforms/access-bridge/src/server.ts new file mode 100644 index 000000000..9bc13080a --- /dev/null +++ b/platforms/access-bridge/src/server.ts @@ -0,0 +1,87 @@ +import { Server as HTTPServer } from 'http'; + +import express, { Express } from 'express'; + +import { Middleware } from './pipeline/middleware.js'; +import logger from './pipeline/logger.js'; + +/** + * Server class that initializes and manages an Express application with error handling. + */ +export class Server { + private app: Express; + private middleware: Middleware; + private httpServer: HTTPServer | null; + private address: string; + private port: number; + + /** + * Creates an instance of the Server class. + * @param address - Address to bind the server to + * @param port - Port to bind the server to + * @param initializeRoutes - Function to register routes + */ + constructor(address: string, port: number, initializeRoutes: (app: Express) => void) { + this.app = express(); + this.middleware = new Middleware(); + this.httpServer = null; + this.address = address; + this.port = port; + this.initialize(initializeRoutes); + } + + /** + * Initializes the server with middlewares and routes. + * @param initializeRoutes - Function to initialize the defined routes + */ + private initialize(initializeRoutes: (app: Express) => void) { + // Initialize core middleware, e.g., CORS, JSON parsing. + this.middleware.initializeCoreMiddleware(this.app); + + // Initialize the defined routes + initializeRoutes(this.app); + + // Initialize Sentry error handler for Expresss + this.middleware.initializeSentryMiddleware(this.app); + + // Initialize error middleware after all routes are registered + this.middleware.initializeErrorMiddleware(this.app); + } + + /** + * Starts the server and listens on the specified port. + * @returns A promise that resolves to the port the server is listening on + */ + public async listen(): Promise { + return new Promise((resolve, reject) => { + this.httpServer = this.app.listen(this.port, this.address, () => { + this.port = (this.httpServer?.address() as { port: number })?.port || this.port; + logger.info(`Server listening at http://${this.address}:${this.port}`); + resolve(this.port); + }); + + this.httpServer.on('error', (err: Error) => { + reject(err); + }); + }); + } + + /** + * Closes the server connection. + * @returns A promise that resolves when the server is closed + */ + public async close(): Promise { + if (!this.httpServer) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + this.httpServer?.close((err?: Error) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } +} diff --git a/platforms/access-bridge/src/services/identity-service.ts b/platforms/access-bridge/src/services/identity-service.ts new file mode 100644 index 000000000..24a3da98f --- /dev/null +++ b/platforms/access-bridge/src/services/identity-service.ts @@ -0,0 +1,31 @@ +import { SIMS_API_HOST } from '../app-config.js'; +import { ErrorDefinitions } from '../errors.js'; +import { get } from '../http.js'; + +export type Viewer = { + id: string; + email: string; +}; + +/** + * Service class responsible for interacting with the Account API that handles identity information. + */ +export class IdentityService { + /** + * Retrieves viewer information based on the provided Authorization token. + * + * @param authorization The Bearer token used to authenticate the request. + * @returns A Promise that resolves to an Account object. + */ + async getAccount({ authorization }: { authorization: string }): Promise { + const account = await get(`${SIMS_API_HOST}/v3/accounts`, authorization); + if (!account) { + throw ErrorDefinitions.NotFoundError.create({ description: 'Account not found.' }); + } + + return { + id: account.id.toString(), + email: account.email, + }; + } +} diff --git a/platforms/access-bridge/src/services/passport-service.ts b/platforms/access-bridge/src/services/passport-service.ts new file mode 100644 index 000000000..8e1cdc20a --- /dev/null +++ b/platforms/access-bridge/src/services/passport-service.ts @@ -0,0 +1,68 @@ +import jwt from 'jsonwebtoken'; +import { PassportResponse } from '@jwp/ott-common/types/passport.js'; +import { AccessControlPlan } from '@jwp/ott-common/types/plans.js'; + +import { ACCESS_CONTROL_API_HOST, API_SECRET, SITE_ID } from '../app-config.js'; +import { put } from '../http.js'; + +type GeneratePassportParams = { + viewerId: string; + plans: AccessControlPlan[]; +}; + +/** + * PassportService handles interactions with the passport APIs. + * It provides methods to generating access tokens (passport and refresh token). + */ +export class PassportService { + /** + * Generate access tokens for a specific site and subscriber. + * @param email The subscriber's email address. + * @param plans Array of access plans for the subscriber. + * @returns A Promise resolving to an AccessTokensResponse. + */ + async generatePassport({ viewerId, plans }: GeneratePassportParams): Promise { + const url = await this.generateSignedUrl(`/v2/sites/${SITE_ID}/access/generate`); + const payload = { + subscriber_info: { + email: viewerId, + plans, + }, + }; + + return await put(url, payload); + } + + /** + * Refresh access tokens for a specific site using a refresh token. + * @param refreshToken The refresh token to use for token refresh. + * @returns A Promise resolving to an AccessTokensResponse. + */ + async refreshPassport({ refreshToken }: { refreshToken: string }): Promise { + const url = await this.generateSignedUrl(`/v2/sites/${SITE_ID}/access/refresh`); + const payload = { + refresh_token: refreshToken, + }; + + return await put(url, payload); + } + + // URL signer - needed for validating requests on Delivery Gateway + // More about this: https://docs.jwplayer.com/platform/reference/protect-your-content-with-signed-urls + async generateSignedUrl(path: string, host: string = ACCESS_CONTROL_API_HOST): Promise { + const now = new Date(); + const token = jwt.sign( + { + // Sets expiration 3.6 seconds from now, rounded up to align with the next 300 ms interval for consistency. + exp: Math.ceil((now.getTime() + 3600) / 300) * 300, + resource: path, + }, + API_SECRET, + { + noTimestamp: true, + } + ); + + return `${host}${path}?token=${token}`; + } +} diff --git a/platforms/access-bridge/src/services/payment-service.ts b/platforms/access-bridge/src/services/payment-service.ts new file mode 100644 index 000000000..3980cc62e --- /dev/null +++ b/platforms/access-bridge/src/services/payment-service.ts @@ -0,0 +1,44 @@ +import type { CheckoutParams, Product } from '@jwp/ott-common/types/payment.js'; + +import { Viewer } from './identity-service'; +/** + * PaymentService interface defines the contract for payment service implementations. + * Any class implementing this should handle products, prices, checkout and payment, + * from a specific payment provider (e.g., Stripe, Google, Apple). + */ +export interface PaymentService { + /** + * Retrieves products with prices based on the provided product IDs. + * The implementation should interact with the payment provider's API to fetch products and prices details. + * + * @param productIds - An array of product IDs to match and filter the provider's store products. + * @returns A Promise that resolves to an array of products, each containing associated price details. + */ + getProductsWithPrices(productIds: string[]): Promise; + + /** + * Creates a checkout session based on the provided viewer and checkout parameters. + * This method should be implemented by each provider's payment service. + * + * @param viewer The viewer making the payment (e.g., their email and ID). + * @param params The generic checkout parameters that will be customized for each payment provider. + * @returns A Promise resolving to a checkout session URL depending on the provider. + */ + createCheckoutSessionUrl(viewer: Viewer, params: CheckoutParams): Promise; + + /** + * Validates the provided checkout parameters based on the specific provider's requirements. + * @param params - The checkout parameters to validate. + * @returns An error string if validation fails, or null if validation succeeds. + */ + validateCheckoutParams(params: CheckoutParams): string | null; + + /** + * Creates a billing portal session URL for the provided viewer and redirectUrl. + * The generated session URL redirects the viewer where they can update their purchase info. + * @param viewer The viewer making the payment (e.g., their email and ID). + * @param returnUrl The url where to redirect them after completion of the updates. + * @returns A Promise resolving to the URL of the billing portal session. + */ + createBillingPortalSessionUrl(viewer: Viewer, returnUrl: string): Promise; +} diff --git a/platforms/access-bridge/src/services/plans-service.ts b/platforms/access-bridge/src/services/plans-service.ts new file mode 100644 index 000000000..effc5da67 --- /dev/null +++ b/platforms/access-bridge/src/services/plans-service.ts @@ -0,0 +1,42 @@ +import { Plan, PlansResponse } from '@jwp/ott-common/types/plans.js'; + +import { SIMS_API_HOST, SITE_ID } from '../app-config.js'; +import { get } from '../http.js'; + +/** + * Service class responsible for interacting with the Plans API. + */ +export class PlansService { + /** + * Fetches a list of plans available for a specific site. + * These are the plans defined by the customer that are available for purchase by viewers. + * + * @returns A Promise that resolves to an array of `Plan` objects. + * If no plans are available, an empty array is returned. + */ + async getAvailablePlans(): Promise { + const response = await get(`${SIMS_API_HOST}/v3/sites/${SITE_ID}/plans`); + if (!response?.plans) { + return []; + } + + return response.plans; + } + + /** + * Fetches a list of plans available / purchased for a specific site by the viewer. + * These plans are stored in the user's passport, enabling access to specific content. + * @param authorization The Bearer token used to authenticate the viewer and their entitlements. + * This parameter can be undefined if only free plans are to be fetched. + * @returns A Promise that resolves to an array of `Plan` objects. + * If no plans are available, an empty array is returned. + */ + async getEntitledPlans({ authorization }: { authorization?: string }): Promise { + const response = await get(`${SIMS_API_HOST}/v3/sites/${SITE_ID}/entitlements`, authorization); + if (!response?.plans) { + return []; + } + + return response.plans; + } +} diff --git a/platforms/access-bridge/src/services/stripe-payment-service.ts b/platforms/access-bridge/src/services/stripe-payment-service.ts new file mode 100644 index 000000000..1dcbb775d --- /dev/null +++ b/platforms/access-bridge/src/services/stripe-payment-service.ts @@ -0,0 +1,184 @@ +import Stripe from 'stripe'; +import { Product, Price, CheckoutParams } from '@jwp/ott-common/types/payment.js'; + +import { STRIPE_SECRET } from '../app-config.js'; +import { AccessBridgeError } from '../errors.js'; +import logger from '../pipeline/logger.js'; + +import { PaymentService } from './payment-service.js'; +import { Viewer } from './identity-service.js'; + +/** + * Service class responsible for interacting with the Stripe API to fetch products. + */ +export class StripePaymentService implements PaymentService { + private stripe: Stripe; + + constructor() { + this.stripe = new Stripe(STRIPE_SECRET, { + // By specifying an API version, we ensure that our integration continues to work + // as expected, even if new versions of the Stripe API are released. + // If no version is specified, the Stripe client will default to the account's current API version, + // which may lead to unexpected behavior if the account is upgraded to a newer API. + apiVersion: '2024-06-20', + }); + } + + /** + * Retrieves Stripe products with prices based on the provided productIds. + * Only products with valid prices are returned. + * @param productIds The array of product IDs to fetch. + * @returns A Promise resolving to an array of filtered Product objects. + */ + async getProductsWithPrices(productIds: string[]): Promise { + if (!productIds.length) { + return []; + } + + const productsWithPrices = await Promise.all( + productIds.map(async (productId) => { + try { + const product = await this.stripe.products.retrieve(productId); + if (!product.active) { + // Only include active products + return null; + } + + const prices = await this.stripe.prices.list({ product: product.id }); + if (!prices.data.length) { + // Only include products with prices + return null; + } + + const mappedPrices = this.mapPrices(prices.data); + + return this.mapProduct(product, mappedPrices); + } catch (error) { + console.error(`Failed to fetch product or prices for product ${productId}:`, error); + return null; // Skip products that fail to fetch prices + } + }) + ); + + // Filter out null products (those that failed to retrieve or have no prices) + return productsWithPrices.filter((product) => product !== null) as Product[]; + } + + /** + * Creates a Stripe Checkout session URL, where the viewer will be redirected to complete the payment. + * @param viewer Email address and viewer id from the auth token used for creating the checkout session. + * @param params Stripe checkout params to use for creating the checkout session. + * @returns A Promise resolving to a Stripe Checkout Session URL for the checkout page. + */ + async createCheckoutSessionUrl(viewer: Viewer, params: CheckoutParams): Promise { + const sessionParams: Stripe.Checkout.SessionCreateParams = { + payment_method_types: ['card'], + line_items: [ + { + price: params.price_id, + quantity: 1, + }, + ], + metadata: { + // This is very important as it's our only way of connecting the payment back to the viewer + viewer_id: viewer.id, + }, + customer_email: viewer.email, + mode: 'subscription', // at this moment we only support subscription mode + success_url: params.success_url, + cancel_url: params.cancel_url, + subscription_data: { + metadata: { + // This is very important as it's our only way of connecting the payment back to the viewer + viewer_id: viewer.id, + }, + }, + }; + + const checkoutSession = await this.stripe.checkout.sessions.create(sessionParams); + return checkoutSession.url; + } + + /** + * Creates a Stripe billing portal session for a given customer ID. + * @param customerId The ID of the customer for whom the session is created. + * @returns A Promise resolving to the URL of the billing portal session. + */ + async createBillingPortalSessionUrl(viewer: Viewer, returnUrl: string): Promise { + const customers = await this.stripe.customers.search({ + query: `email:"${viewer.email}"`, + }); + + if (customers.data.length === 0) { + logger.info(`Viewer 'id: ${viewer.id}, email: ${viewer.email}' does not exist in Stripe.`); + throw new AccessBridgeError('NotFoundError', { + description: `Viewer does not exist in Stripe.`, + }); + } + + // Create the billing portal session using the retrieved customer ID + const customerId = customers.data[0].id; + const session = await this.stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: returnUrl, + }); + + return session.url; + } + + /** + * Validates the provided checkout parameters. + * Checks for the presence of required fields: 'price_id', 'success_url', and 'cancel_url'. + * If any required parameter is missing, returns an error message; otherwise, returns null. + * @param params - The checkout parameters to validate. + * @returns A string containing the name of the missing parameter if validation fails, + * or null if all required parameters are present. + */ + validateCheckoutParams(params: CheckoutParams): string | null { + const requiredParams: (keyof CheckoutParams)[] = ['price_id', 'success_url', 'cancel_url']; + const missingParam = requiredParams.find((param) => !params[param]); + return missingParam ? `Missing required parameter: ${missingParam}` : null; + } + + /** + * Maps the Stripe product to our custom Product type. + * @param product The Stripe product object. + * @param prices The list of custom Price objects mapped from Stripe prices. + * @returns A Product object with the required fields. + */ + private mapProduct(product: Stripe.Product, prices: Price[]): Product { + return { + store_product_id: product.id, + name: product.name, + description: product.description ?? '', + default_store_price_id: product.default_price as string, + prices: prices, + }; + } + + /** + * Maps Stripe prices to our custom Price type. + * @param stripePrices The list of Stripe price objects. + * @returns A list of custom Price objects. + */ + private mapPrices(stripePrices: Stripe.Price[]): Price[] { + return stripePrices.map((price) => ({ + store_price_id: price.id, + currencies: { + [price.currency]: { + amount: price.unit_amount, + }, + }, + default_currency: price.currency, + recurrence: price.recurring + ? { + interval: price.recurring.interval, + duration: price.recurring.interval_count ?? 1, + trial_period_interval: 'day', // Stripe only supports day for trial period. + trial_period_duration: price.recurring.trial_period_days ?? null, + } + : 'one_time', // Set 'one_time' if there's no recurrence. + billing_scheme: 'per_unit', // We only support `per_unit` scheme. + })); + } +} diff --git a/platforms/access-bridge/stylelint.config.cjs b/platforms/access-bridge/stylelint.config.cjs new file mode 100644 index 000000000..67e3aa538 --- /dev/null +++ b/platforms/access-bridge/stylelint.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['stylelint-config-jwp'], +}; diff --git a/platforms/access-bridge/test/fixtures.ts b/platforms/access-bridge/test/fixtures.ts new file mode 100644 index 000000000..503850ae8 --- /dev/null +++ b/platforms/access-bridge/test/fixtures.ts @@ -0,0 +1,200 @@ +import Stripe from 'stripe'; +import { Plan } from '@jwp/ott-common/types/plans.js'; +import { Price, Product } from '@jwp/ott-common/types/payment.js'; + +import { Viewer } from '../src/services/identity-service'; +import { ErrorDefinitions } from '../src/errors.js'; + +// Utility function to get Unix timestamp +export const getTimestamp = (daysOffset: number): number => { + const now = new Date(); + now.setDate(now.getDate() + daysOffset); + return Math.floor(now.getTime() / 1000); +}; + +// Precompute timestamps +const FUTURE_EXPIRY = getTimestamp(30); // 30 days from now +const PAST_EXPIRY = getTimestamp(-30); // 30 days ago + +// API endpoints constant +export const ENDPOINTS = { + GENERATE_PASSPORT: '/v2/sites/:site_id/access/generate', + REFRESH_PASSPORT: '/v2/sites/:site_id/access/refresh', + PRODUCTS: '/v2/sites/:site_id/products', + CHECKOUT: '/v2/sites/:site_id/checkout', + BILLING_PORTAL: '/v2/sites/:site_id/billing-portal', +}; + +// mock data for access tokens +export const ACCESS_TOKENS = { + PASSPORT: { + VALID: 'valid-passport', + INVALID: 'invalid-passport', + }, + REFRESH_TOKEN: { + VALID: 'valid-refresh-token', + INVALID: 'invalid-refresh-token', + }, +}; + +export const VALID_PLAN_ID = 'plan1234'; +export const VIEWER: Viewer = { + id: '123456', + email: 'dummy@test.com', +}; + +// Plan mock creation function +const createMockPlan = ({ id, exp, access_model, access, metadata }: Plan): Plan => ({ + id, + exp, + access_model, + access, + metadata, +}); + +export const PLANS = { + VALID: [ + createMockPlan({ + id: 'plan1234', + exp: FUTURE_EXPIRY, + access_model: 'svod', + access: { + drm_policy_id: 'drm_policy_123', + }, + metadata: { + external_providers: { + stripe: 'stripe_id', + }, + }, + }), + ], + FREE: [ + createMockPlan({ + id: 'free1234', + exp: FUTURE_EXPIRY, + access_model: 'free', + access: { + drm_policy_id: 'drm_policy_456', + }, + metadata: { + external_providers: {}, + }, + }), + ], + INVALID: [ + createMockPlan({ + id: 'plan123456', + exp: FUTURE_EXPIRY, + access_model: 'svod', + access: { + drm_policy_id: 'drm_policy_789', + }, + metadata: { + external_providers: {}, + }, + }), + ], + EXPIRED: [ + createMockPlan({ + id: 'plan1234', + exp: PAST_EXPIRY, + access_model: 'svod', + access: { + drm_policy_id: 'drm_policy_101', + }, + metadata: { + external_providers: {}, + }, + }), + ], +}; + +// Valid and invalid site id mock +export const SITE_ID = { + VALID: 'test1234', + VALID_UPPER: 'A1B2C3D4', + INVALID: 'invalid1234', + SHORT: 'abc123', + LONG: 'abcd12345', + SPECIAL: 'abcd123!', + EMPTY: '', +}; + +// Authorization mock - valid and invalid token +export const AUTHORIZATION = { + VALID: 'Bearer valid-authorization', + INVALID: 'Bearer invalid-authorization', + MISSING: '', +}; + +// Store price mock +export const STORE_PRICE: Price = { + store_price_id: 'price_123456789', + currencies: { + usd: { + amount: 1000, // Amount in cents for USD + }, + }, + default_currency: 'usd', + recurrence: { + interval: 'month', + duration: 1, // Occurs every 1 month + trial_period_interval: 'month', // Free trial is based on months + trial_period_duration: 1, // 1 month trial + }, + billing_scheme: 'per_unit', // Only per_unit supported for now +}; + +// Store product mock +export const STORE_PRODUCT: Product = { + store_product_id: 'prod_123456789', + name: 'Sample Product', + description: 'A high-quality product description', + default_store_price_id: 'price_123456789', + prices: [STORE_PRICE], +}; + +// Dummy stripe customer id +export const STRIPE_CUSTOMER_ID = 'cus_Qi45IcSi81LstA'; + +// mock of the handled error cases for Stripe +export const STRIPE_ERRORS = [ + { + error: new Stripe.errors.StripeInvalidRequestError({ + type: 'invalid_request_error', + message: 'Invalid request', + }), + expectedCode: ErrorDefinitions.BadRequestError.code, + statusCode: 400, + }, + + { + error: new Stripe.errors.StripeAuthenticationError({ + type: 'authentication_error', + message: 'Not authenticated.', + }), + expectedCode: ErrorDefinitions.UnauthorizedError.code, + statusCode: 401, + }, + + { + error: new Stripe.errors.StripePermissionError({ + type: 'invalid_grant', + message: 'Permission error request.', + }), + expectedCode: ErrorDefinitions.ForbiddenError.code, + statusCode: 403, + }, + + { + error: new Stripe.errors.StripeAPIError({ + type: 'api_error', + message: 'Invalid request', + }), + expectedCode: ErrorDefinitions.BadRequestError.code, + statusCode: 400, + }, +]; + +// mock of stripe session url +export const STRIPE_SESSION_URL = 'https://example.com'; diff --git a/platforms/access-bridge/test/integration/access.test.ts b/platforms/access-bridge/test/integration/access.test.ts new file mode 100644 index 000000000..7e5de10a6 --- /dev/null +++ b/platforms/access-bridge/test/integration/access.test.ts @@ -0,0 +1,180 @@ +import http from 'http'; + +import { Express } from 'express'; +import { describe, it, beforeAll, afterAll, expect } from 'vitest'; + +import { AccessController } from '../../src/controllers/access-controller.js'; +import { MockServer } from '../mock-server.js'; +import { ACCESS_TOKENS, AUTHORIZATION, ENDPOINTS, SITE_ID } from '../fixtures.js'; +import { MockAccessController } from '../mocks/access.js'; +import { ErrorDefinitions } from '../../src/errors.js'; +import { addRoute } from '../../src/pipeline/routes.js'; +import { validateSiteId } from '../mocks/middleware.js'; + +describe('AccessController tests', () => { + let mockServer: MockServer; + let accessController: AccessController; + + beforeAll(async () => { + accessController = new MockAccessController(); + + const initializeRoutes = (app: Express) => { + addRoute(app, 'put', ENDPOINTS.GENERATE_PASSPORT, accessController.generatePassport.bind(accessController), [ + validateSiteId, + ]); + addRoute(app, 'put', ENDPOINTS.REFRESH_PASSPORT, accessController.refreshPassport.bind(accessController), [ + validateSiteId, + ]); + }; + + mockServer = await MockServer.create(initializeRoutes); + }); + + const generatePassportTestCases = [ + { + description: 'should generate passport access tokens without authorization', + requestOptions: { + method: 'PUT', + path: ENDPOINTS.GENERATE_PASSPORT.replace(':site_id', SITE_ID.VALID), + }, + expectedStatusCode: 200, + expectedResponse: { + passport: ACCESS_TOKENS.PASSPORT.VALID, + refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID, + }, + }, + { + description: 'should generate passport access tokens with valid authorization', + requestOptions: { + headers: { Authorization: AUTHORIZATION.VALID }, + method: 'PUT', + path: ENDPOINTS.GENERATE_PASSPORT.replace(':site_id', SITE_ID.VALID), + }, + expectedStatusCode: 200, + expectedResponse: { + passport: ACCESS_TOKENS.PASSPORT.VALID, + refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID, + }, + }, + { + description: 'should return UnauthorizedError for invalid authorization', + requestOptions: { + headers: { Authorization: AUTHORIZATION.INVALID }, + method: 'PUT', + path: ENDPOINTS.GENERATE_PASSPORT.replace(':site_id', SITE_ID.VALID), + }, + expectedStatusCode: 401, + expectedError: ErrorDefinitions.UnauthorizedError.code, + }, + { + description: 'should return ParameterInvalidError for invalid site_id', + requestOptions: { + headers: { Authorization: AUTHORIZATION.VALID }, + method: 'PUT', + path: ENDPOINTS.GENERATE_PASSPORT.replace(':site_id', SITE_ID.INVALID), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterInvalidError.code, + }, + ]; + + const refreshPassportTestCases = [ + { + description: 'should refresh passport access tokens with valid refresh_token', + requestOptions: { + headers: { + 'Content-Type': 'application/json', + }, + method: 'PUT', + path: ENDPOINTS.REFRESH_PASSPORT.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID, + }), + }, + expectedStatusCode: 200, + expectedResponse: { + passport: ACCESS_TOKENS.PASSPORT.VALID, + refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID, + }, + }, + { + description: 'should not generate access tokens with invalid site_id', + requestOptions: { + headers: { + 'Content-Type': 'application/json', + }, + method: 'PUT', + path: ENDPOINTS.REFRESH_PASSPORT.replace(':site_id', SITE_ID.INVALID), + body: JSON.stringify({ + refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID, + }), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterInvalidError.code, + }, + { + description: 'should fail with forbidden for invalid refresh_token provided', + requestOptions: { + headers: { + 'Content-Type': 'application/json', + }, + method: 'PUT', + path: ENDPOINTS.REFRESH_PASSPORT.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.INVALID, + }), + }, + expectedStatusCode: 403, + expectedError: ErrorDefinitions.ForbiddenError.code, + }, + { + description: 'should return ParameterMissingError for missing refresh_token', + requestOptions: { + headers: { + 'Content-Type': 'application/json', + }, + method: 'PUT', + path: ENDPOINTS.REFRESH_PASSPORT.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + // missing refresh_token + }), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterMissingError.code, + }, + ]; + + const allTestCases = [...generatePassportTestCases, ...refreshPassportTestCases]; + + it.each(allTestCases)( + '$description', + async ({ requestOptions, expectedStatusCode, expectedResponse, expectedError }) => { + const response = await new Promise((resolve) => { + mockServer.request(requestOptions, resolve).end(); + }); + expect(response.statusCode).toBe(expectedStatusCode); + + const body = await new Promise((resolve) => { + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + response.on('end', () => { + resolve(data); + }); + }); + + const responseBody = JSON.parse(body); + + if (expectedResponse) { + expect(responseBody).toMatchObject(expectedResponse); + } else if (expectedError) { + expect(responseBody.errors[0].code).toBe(expectedError); + } + } + ); + + afterAll(async () => { + await mockServer.close(); + }); +}); diff --git a/platforms/access-bridge/test/integration/checkout.test.ts b/platforms/access-bridge/test/integration/checkout.test.ts new file mode 100644 index 000000000..72612fd88 --- /dev/null +++ b/platforms/access-bridge/test/integration/checkout.test.ts @@ -0,0 +1,259 @@ +import http from 'http'; + +import { Express } from 'express'; +import { describe, it, beforeAll, afterAll, expect } from 'vitest'; + +import { MockServer } from '../mock-server.js'; +import { ErrorDefinitions } from '../../src/errors.js'; +import { + STRIPE_SESSION_URL, + VALID_PLAN_ID, + ENDPOINTS, + STORE_PRODUCT, + STRIPE_ERRORS, + AUTHORIZATION, + SITE_ID, + STORE_PRICE, +} from '../fixtures.js'; +import { MockCheckoutController } from '../mocks/checkout.js'; +import { addRoute } from '../../src/pipeline/routes.js'; +import { validateSiteId } from '../mocks/middleware.js'; + +describe('CheckoutController tests', () => { + let mockServer: MockServer; + let checkoutController: MockCheckoutController; + + beforeAll(async () => { + checkoutController = new MockCheckoutController(); + + const initializeRoutes = (app: Express) => { + addRoute(app, 'post', ENDPOINTS.CHECKOUT, checkoutController.initiateCheckout.bind(checkoutController), [ + validateSiteId, + ]); + addRoute( + app, + 'post', + ENDPOINTS.BILLING_PORTAL, + checkoutController.generateBillingPortalURL.bind(checkoutController), + [validateSiteId] + ); + }; + + mockServer = await MockServer.create(initializeRoutes); + }); + + const checkoutTestCases = [ + { + description: 'should initiate checkout session successfully', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.CHECKOUT.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + price_id: STORE_PRODUCT.store_product_id, + success_url: 'http://example.com', + cancel_url: 'http://example.com', + }), + }, + expectedStatusCode: 200, + expectedResponse: { + url: STRIPE_SESSION_URL, + }, + }, + { + description: 'should return ParameterInvalidError for invalid site_id', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.CHECKOUT.replace(':site_id', SITE_ID.INVALID), + body: JSON.stringify({ + price_id: STORE_PRICE.store_price_id, + success_url: 'http://example.com', + cancel_url: 'http://example.com', + }), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterInvalidError.code, + }, + { + description: 'should return UnauthorizedError for missing authorization token', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.MISSING, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.CHECKOUT.replace(':site_id', SITE_ID.VALID), + }, + expectedStatusCode: 401, + expectedError: ErrorDefinitions.UnauthorizedError.code, + }, + { + description: 'should handle missing required parameters', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.CHECKOUT.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + price_id: STORE_PRICE.store_price_id, + // missing success_url + // missing cancel_url + }), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterMissingError.code, + }, + ]; + + const billingPortalTestCases = [ + { + description: 'should create billing portal URL successfully', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.BILLING_PORTAL.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + return_url: 'http://example.com', + }), + }, + expectedStatusCode: 200, + expectedResponse: { + url: STRIPE_SESSION_URL, + }, + }, + { + description: 'should return ParameterInvalidError for invalid site_id', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.CHECKOUT.replace(':site_id', SITE_ID.INVALID), + body: JSON.stringify({ + return_url: 'http://example.com', + }), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterInvalidError.code, + }, + { + description: 'should return UnauthorizedError for missing authorization token in billing portal', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.MISSING, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.BILLING_PORTAL.replace(':site_id', SITE_ID.VALID), + }, + expectedStatusCode: 401, + expectedError: ErrorDefinitions.UnauthorizedError.code, + }, + { + description: 'should handle missing required parameters in billing portal', + requestOptions: { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.BILLING_PORTAL.replace(':site_id', SITE_ID.VALID), + body: JSON.stringify({ + // missing return_url + }), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterMissingError.code, + }, + ]; + + const allTestCases = [...checkoutTestCases, ...billingPortalTestCases]; + + it.each(allTestCases)( + '$description', + async ({ requestOptions, expectedStatusCode, expectedResponse, expectedError }) => { + const response = await new Promise((resolve) => { + mockServer.request(requestOptions, resolve).end(); + }); + expect(response.statusCode).toBe(expectedStatusCode); + + const body = await new Promise((resolve) => { + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + response.on('end', () => { + resolve(data); + }); + }); + + const responseBody = JSON.parse(body); + + if (expectedResponse) { + expect(responseBody).toMatchObject(expectedResponse); + } else if (expectedError) { + expect(responseBody.errors[0].code).toBe(expectedError); + } + } + ); + + STRIPE_ERRORS.forEach(({ error, expectedCode, statusCode }) => { + it(`should handle ${error.type} correctly`, async () => { + checkoutController['paymentService'].setMockBehavior('error', error); + + const requestBody = JSON.stringify({ + access_plan_id: VALID_PLAN_ID, + price_id: STORE_PRICE.store_price_id, + mode: 'subscription', + success_url: 'http://example.com', + cancel_url: 'http://example.com', + }); + + const requestOptions = { + headers: { + Authorization: AUTHORIZATION.VALID, + 'Content-Type': 'application/json', + }, + method: 'POST', + path: ENDPOINTS.CHECKOUT.replace(':site_id', SITE_ID.VALID), + body: requestBody, + }; + + const response = await new Promise((resolve) => { + mockServer.request(requestOptions, resolve).end(); + }); + + expect(response.statusCode).toBe(statusCode); + + const body = await new Promise((resolve) => { + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + response.on('end', () => { + resolve(data); + }); + }); + + const responseBody = JSON.parse(body); + expect(responseBody.errors[0].code).toBe(expectedCode); + }); + }); + + afterAll(async () => { + await mockServer.close(); + }); +}); diff --git a/platforms/access-bridge/test/integration/products.test.ts b/platforms/access-bridge/test/integration/products.test.ts new file mode 100644 index 000000000..332c1c15a --- /dev/null +++ b/platforms/access-bridge/test/integration/products.test.ts @@ -0,0 +1,136 @@ +import http from 'http'; + +import { Express } from 'express'; +import { describe, it, beforeAll, afterAll, expect } from 'vitest'; + +import { MockServer } from '../mock-server.js'; +import { ENDPOINTS, SITE_ID, STRIPE_ERRORS, STORE_PRODUCT } from '../fixtures.js'; +import { ErrorDefinitions } from '../../src/errors.js'; +import { MockBehavior, MockPaymentService, MockProductsController } from '../mocks/products.js'; +import { addRoute } from '../../src/pipeline/routes.js'; +import { validateSiteId } from '../mocks/middleware.js'; + +describe('ProductsController tests', () => { + let mockServer: MockServer; + let productsController: MockProductsController; + + beforeAll(async () => { + productsController = new MockProductsController(); + + const initializeRoutes = (app: Express) => { + addRoute(app, 'get', ENDPOINTS.PRODUCTS, productsController.getProducts.bind(productsController), [ + validateSiteId, + ]); + }; + + mockServer = await MockServer.create(initializeRoutes); + }); + + const testCases = [ + { + description: 'should list stripe products', + requestOptions: { + method: 'GET', + path: ENDPOINTS.PRODUCTS.replace(':site_id', SITE_ID.VALID), + }, + mockBehavior: 'default' as MockBehavior, + expectedStatusCode: 200, + expectedResponse: [STORE_PRODUCT], + }, + { + description: 'should handle empty products', + requestOptions: { + method: 'GET', + path: ENDPOINTS.PRODUCTS.replace(':site_id', SITE_ID.VALID), + }, + mockBehavior: 'empty' as MockBehavior, + expectedStatusCode: 200, + expectedResponse: [], + }, + { + description: 'should return ParameterInvalidError for invalid site_id', + requestOptions: { + method: 'GET', + path: ENDPOINTS.PRODUCTS.replace(':site_id', SITE_ID.INVALID), + }, + expectedStatusCode: 400, + expectedError: ErrorDefinitions.ParameterInvalidError.code, + }, + { + description: 'should return NotFoundError for invalid route', + requestOptions: { + method: 'GET', + path: `${ENDPOINTS.PRODUCTS.replace(':site_id', SITE_ID.INVALID)}/invalid`, + }, + expectedStatusCode: 404, + expectedError: ErrorDefinitions.NotFoundError.code, + }, + ]; + + it.each(testCases)( + '$description', + async ({ requestOptions, mockBehavior, expectedStatusCode, expectedResponse, expectedError }) => { + if (mockBehavior) { + (productsController['paymentService'] as MockPaymentService).setMockBehavior(mockBehavior); + } + + const response = await new Promise((resolve) => { + mockServer.request(requestOptions, resolve).end(); + }); + + expect(response.statusCode).toBe(expectedStatusCode); + + const body = await new Promise((resolve) => { + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + response.on('end', () => { + resolve(data); + }); + }); + + const responseBody = JSON.parse(body); + + if (expectedResponse) { + expect(responseBody).toMatchObject(expectedResponse); + } else if (expectedError) { + expect(responseBody.errors[0].code).toBe(expectedError); + } + } + ); + + STRIPE_ERRORS.forEach(({ error, expectedCode, statusCode }) => { + it(`should handle ${error.type} correctly`, async () => { + (productsController['paymentService'] as MockPaymentService).setMockBehavior('error', error); + + const requestOptions = { + method: 'GET', + path: ENDPOINTS.PRODUCTS.replace(':site_id', SITE_ID.VALID), + }; + + const response = await new Promise((resolve) => { + mockServer.request(requestOptions, resolve).end(); + }); + + expect(response.statusCode).toBe(statusCode); + + const body = await new Promise((resolve) => { + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + response.on('end', () => { + resolve(data); + }); + }); + + const responseBody = JSON.parse(body); + expect(responseBody.errors[0].code).toBe(expectedCode); + }); + }); + + afterAll(async () => { + await mockServer.close(); + }); +}); diff --git a/platforms/access-bridge/test/mock-server.ts b/platforms/access-bridge/test/mock-server.ts new file mode 100644 index 000000000..60f5c7f35 --- /dev/null +++ b/platforms/access-bridge/test/mock-server.ts @@ -0,0 +1,48 @@ +import http from 'http'; +import { RequestOptions } from 'https'; + +import { Express } from 'express'; + +import { Server } from '../src/server.js'; + +interface ExtendedRequestOptions extends RequestOptions { + body?: string; +} + +export class MockServer { + private server: Server; + readonly port: number; + + constructor(server: Server, port: number) { + this.server = server; + this.port = port; + } + + static async create(initializeRoutes: (app: Express) => void): Promise { + // Use port 0 to let the OS select an available port for testing + const server = new Server('localhost', 0, initializeRoutes); + const port = await server.listen(); + return new this(server, port); + } + + addRequestOptions(options: http.RequestOptions): http.RequestOptions { + options.host = 'localhost'; + options.port = this.port; + return options; + } + + request(options: ExtendedRequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest { + const req = http.request(this.addRequestOptions(options), callback); + + if (options.body) { + req.write(options.body); + } + + req.end(); + return req; + } + + async close(): Promise { + await this.server.close(); + } +} diff --git a/platforms/access-bridge/test/mocks/access.ts b/platforms/access-bridge/test/mocks/access.ts new file mode 100644 index 000000000..e2de4ea91 --- /dev/null +++ b/platforms/access-bridge/test/mocks/access.ts @@ -0,0 +1,61 @@ +import { Plan } from '@jwp/ott-common/types/plans.js'; + +import { PassportService } from '../../src/services/passport-service.js'; +import { PlansService } from '../../src/services/plans-service.js'; +import { ACCESS_TOKENS, PLANS, AUTHORIZATION, VIEWER } from '../fixtures.js'; +import { IdentityService } from '../../src/services/identity-service.js'; +import { AccessController } from '../../src/controllers/access-controller.js'; +import { ErrorDefinitions } from '../../src/errors.js'; + +// Mock IdentityService +class MockIdentityService extends IdentityService { + async getAccount({ authorization }: { authorization: string }) { + if (authorization === AUTHORIZATION.INVALID) { + throw ErrorDefinitions.UnauthorizedError.create(); + } + + return VIEWER; + } +} + +// Mock PassportService +class MockPassportService extends PassportService { + async generatePassport() { + return { passport: ACCESS_TOKENS.PASSPORT.VALID, refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID }; + } + + async refreshPassport({ refreshToken }: { refreshToken: string }) { + if (!refreshToken) { + throw ErrorDefinitions.ParameterMissingError.create({}); + } + if (refreshToken === ACCESS_TOKENS.REFRESH_TOKEN.INVALID) { + throw ErrorDefinitions.ForbiddenError.create({ description: 'Invalid refresh_token provided' }); + } + return { passport: ACCESS_TOKENS.PASSPORT.VALID, refresh_token: ACCESS_TOKENS.REFRESH_TOKEN.VALID }; + } +} + +// Mock PlansService +class MockPlansService extends PlansService { + async getAvailablePlans(): Promise { + return PLANS.VALID; + } + + async getEntitledPlans({ authorization }: { authorization: string }): Promise { + if (!authorization) { + // if no auth, only free plans available + return PLANS.FREE; + } + + return PLANS.VALID; + } +} + +export class MockAccessController extends AccessController { + constructor() { + super(); + Reflect.set(this, 'identityService', new MockIdentityService()); + Reflect.set(this, 'passportService', new MockPassportService()); + Reflect.set(this, 'plansService', new MockPlansService()); + } +} diff --git a/platforms/access-bridge/test/mocks/checkout.ts b/platforms/access-bridge/test/mocks/checkout.ts new file mode 100644 index 000000000..3eef709cb --- /dev/null +++ b/platforms/access-bridge/test/mocks/checkout.ts @@ -0,0 +1,73 @@ +import { NextFunction, Request, Response } from 'express'; + +import { ErrorDefinitions, sendErrors } from '../../src/errors.js'; +import { AUTHORIZATION, VIEWER } from '../fixtures.js'; +import { IdentityService } from '../../src/services/identity-service.js'; + +import { MockStripePaymentService } from './payment.js'; + +// Mock IdentityService +class MockIdentityService extends IdentityService { + async getAccount({ authorization }: { authorization: string }) { + if (authorization === AUTHORIZATION.INVALID) { + throw ErrorDefinitions.UnauthorizedError.create(); + } + + return VIEWER; + } +} + +// Mock Controller +export class MockCheckoutController { + private identityService: MockIdentityService; + private paymentService: MockStripePaymentService; + + constructor() { + this.identityService = new MockIdentityService(); + this.paymentService = new MockStripePaymentService(); + } + + async initiateCheckout(req: Request, res: Response, next: NextFunction) { + const authorization = req.headers['authorization']; + if (!authorization) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + + const checkoutParams = req.body; + const validationError = this.paymentService.validateCheckoutParams(checkoutParams); + if (validationError) { + sendErrors(res, ErrorDefinitions.ParameterMissingError.create({ parameterName: validationError })); + return; + } + + const viewer = await this.identityService.getAccount({ authorization }); + const checkoutSessionUrl = await this.paymentService.createCheckoutSessionUrl(viewer, checkoutParams); + + res.end(JSON.stringify({ url: checkoutSessionUrl })); + } + + async generateBillingPortalURL(req: Request, res: Response, next: NextFunction) { + const authorization = req.headers['authorization']; + if (!authorization) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + + // Get the email address from the Authorization token + const viewer = await this.identityService.getAccount({ authorization }); + if (!viewer.id || !viewer.email) { + sendErrors(res, ErrorDefinitions.UnauthorizedError.create()); + return; + } + + const { return_url } = req.body; + if (!return_url) { + sendErrors(res, ErrorDefinitions.ParameterMissingError.create({ parameterName: 'return_url' })); + return; + } + + const billingPortalSessionUrl = await this.paymentService.createBillingPortalSessionUrl(viewer, return_url); + res.json({ url: billingPortalSessionUrl }); + } +} diff --git a/platforms/access-bridge/test/mocks/middleware.ts b/platforms/access-bridge/test/mocks/middleware.ts new file mode 100644 index 000000000..470f8b356 --- /dev/null +++ b/platforms/access-bridge/test/mocks/middleware.ts @@ -0,0 +1,17 @@ +import { NextFunction, Request, Response } from 'express'; + +import { ErrorDefinitions, sendErrors } from '../../src/errors'; +import { SITE_ID } from '../fixtures'; + +/** + * Mock Middleware to validate 'site_id' parameter + */ +export const validateSiteId = (req: Request, res: Response, next: NextFunction) => { + if (req.params.site_id !== SITE_ID.VALID) { + sendErrors(res, ErrorDefinitions.ParameterInvalidError.create({ parameterName: 'site_id' })); + return; + } + + // If valid, move to the next middleware or controller + next(); +}; diff --git a/platforms/access-bridge/test/mocks/payment.ts b/platforms/access-bridge/test/mocks/payment.ts new file mode 100644 index 000000000..4eb2fef26 --- /dev/null +++ b/platforms/access-bridge/test/mocks/payment.ts @@ -0,0 +1,74 @@ +import Stripe from 'stripe'; +import { CheckoutParams } from '@jwp/ott-common/types/payment'; + +import { PaymentService } from '../../src/services/payment-service'; +import { AccessBridgeError, ErrorDefinitions } from '../../src/errors'; +import { STORE_PRODUCT, STRIPE_SESSION_URL } from '../fixtures'; +import { Viewer } from '../../src/services/identity-service'; + +import { MockBehavior } from './products'; + +export interface MockPaymentService extends PaymentService { + setMockBehavior(behavior: 'default' | 'empty' | 'error', error?: Stripe.errors.StripeError): unknown; +} + +// Mock StripePaymentService +export class MockStripePaymentService implements MockPaymentService { + private mockBehavior: MockBehavior = 'default'; + private mockError: AccessBridgeError | null = null; + + async getProductsWithPrices(productIds: string[]) { + if (this.mockBehavior === 'error' && this.mockError) { + throw this.mockError; + } + + if (this.mockBehavior === 'empty') { + return []; + } + + return [STORE_PRODUCT]; + } + + async createCheckoutSessionUrl(viewer: Viewer, params: CheckoutParams): Promise { + if (this.mockBehavior === 'error' && this.mockError) { + throw this.mockError; + } + + return STRIPE_SESSION_URL; + } + + async createBillingPortalSessionUrl(viewer: Viewer, returnUrl: string): Promise { + if (this.mockBehavior === 'error' && this.mockError) { + throw this.mockError; + } + + return STRIPE_SESSION_URL; + } + + validateCheckoutParams(params: CheckoutParams): string | null { + const requiredParams: (keyof CheckoutParams)[] = ['price_id', 'success_url', 'cancel_url']; + const missingParam = requiredParams.find((param) => !params[param]); + return missingParam ? `Missing required parameter: ${missingParam}` : null; + } + + // Method to set the mock behavior + setMockBehavior(behavior: 'default' | 'empty' | 'error', error?: Stripe.errors.StripeError) { + this.mockBehavior = behavior; + + if (behavior === 'error' && error instanceof Stripe.errors.StripeError) { + switch (error.type) { + case 'StripeInvalidRequestError': + this.mockError = ErrorDefinitions.BadRequestError.create(); + break; + case 'StripeAuthenticationError': + this.mockError = ErrorDefinitions.UnauthorizedError.create(); + break; + case 'StripePermissionError': + this.mockError = ErrorDefinitions.ForbiddenError.create(); + break; + default: + this.mockError = ErrorDefinitions.BadRequestError.create(); + } + } + } +} diff --git a/platforms/access-bridge/test/mocks/products.ts b/platforms/access-bridge/test/mocks/products.ts new file mode 100644 index 000000000..95b305095 --- /dev/null +++ b/platforms/access-bridge/test/mocks/products.ts @@ -0,0 +1,30 @@ +import Stripe from 'stripe'; +import { Plan } from '@jwp/ott-common/types/plans.js'; + +import { PlansService } from '../../src/services/plans-service.js'; +import { PLANS } from '../fixtures.js'; +import { ProductsController } from '../../src/controllers/products-controller.js'; +import { PaymentService } from '../../src/services/payment-service.js'; + +import { MockStripePaymentService } from './payment.js'; + +export type MockBehavior = 'default' | 'empty' | 'error'; + +export interface MockPaymentService extends PaymentService { + setMockBehavior(behavior: 'default' | 'empty' | 'error', error?: Stripe.errors.StripeError): unknown; +} + +// Mock PlansService +export class MockPlansService extends PlansService { + async getAvailablePlans(): Promise { + return PLANS.VALID; + } +} + +export class MockProductsController extends ProductsController { + constructor() { + super(); + Reflect.set(this, 'paymentService', new MockStripePaymentService()); + Reflect.set(this, 'plansService', new MockPlansService()); + } +} diff --git a/platforms/access-bridge/test/unit/errors.test.ts b/platforms/access-bridge/test/unit/errors.test.ts new file mode 100644 index 000000000..401598d28 --- /dev/null +++ b/platforms/access-bridge/test/unit/errors.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; + +import { ErrorDefinitions } from '../../src/errors.js'; + +describe('AccessBridgeError', () => { + // Test for each error definition + for (const [key, definition] of Object.entries(ErrorDefinitions)) { + const errorKey = key as keyof typeof ErrorDefinitions; + const { code, statusCode, description } = definition; + + it(`should create ${errorKey} with the correct code and status code`, () => { + const error = definition.create({}); + expect(error.code).toBe(code); + expect(error.statusCode).toBe(statusCode); + + // Test default description + const expectedDescription = description.replace('{parameterName}', '').replace('{reason}', ''); + expect(error.description).toBe(expectedDescription); + }); + + it(`should create ${errorKey} with a custom description`, () => { + // Define context based on error type + const context = (() => { + switch (errorKey) { + case 'ParameterMissingError': + return { parameterName: 'testParam' }; + case 'ParameterInvalidError': + return { parameterName: 'testParam', reason: 'Invalid reason' }; + default: + return {}; + } + })(); + + // Define the custom description + const customDescription = (() => { + switch (errorKey) { + case 'ParameterMissingError': + return `Required parameter ${context.parameterName} is missing.`; + case 'ParameterInvalidError': + return `Parameter ${context.parameterName} is invalid. ${context.reason || ''}`; + default: + return description; + } + })(); + + // Create the error with context and custom description + const error = definition.create({ + ...context, + description: customDescription, + }); + + // Check that the description matches + expect(error.description).toBe(customDescription); + }); + } +}); diff --git a/platforms/access-bridge/test/unit/logger.test.ts b/platforms/access-bridge/test/unit/logger.test.ts new file mode 100644 index 000000000..a12c386a9 --- /dev/null +++ b/platforms/access-bridge/test/unit/logger.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as Sentry from '@sentry/node'; + +import logger from '../../src/pipeline/logger.js'; + +describe('Logger Tests', () => { + beforeEach(() => { + // Reset all mocks to ensure a clean slate for each test + vi.resetAllMocks(); + }); + + describe('when Sentry is configured', () => { + const mockCaptureException = vi.fn(); + const mockCaptureMessage = vi.fn(); + + beforeEach(() => { + // Mock Sentry's getClient to return an object with captureException and captureMessage + vi.mocked(Sentry.getClient).mockReturnValue({ + captureException: mockCaptureException, + captureMessage: mockCaptureMessage, + } as unknown as Sentry.NodeClient); + + // Ensure Sentry's captureException and captureMessage methods are also mocked + vi.mocked(Sentry.captureException).mockImplementation(mockCaptureException); + vi.mocked(Sentry.captureMessage).mockImplementation(mockCaptureMessage); + }); + + it('should call Sentry.captureException on error', () => { + const error = new Error('Test error'); + const message = 'Test error message'; + + // Call logger.error which should trigger Sentry.captureException + logger.error(message, error); + + // Verify that captureException was called with the correct arguments + expect(mockCaptureException).toHaveBeenCalledWith(error); + }); + + it('should call Sentry.captureMessage on info', () => { + const message = 'Test info message'; + + // Call logger.info which should trigger Sentry.captureMessage with 'info' + logger.info(message); + + // Verify that captureMessage was called with the correct arguments + expect(mockCaptureMessage).toHaveBeenCalledWith(message, 'info'); + }); + + it('should call Sentry.captureMessage on warn', () => { + const message = 'Test warn message'; + + // Call logger.warn which should trigger Sentry.captureMessage with 'warning' + logger.warn(message); + + // Verify that captureMessage was called with the correct arguments + expect(mockCaptureMessage).toHaveBeenCalledWith(message, 'warning'); + }); + }); + + describe('when Sentry is not configured', () => { + beforeEach(() => { + // Mock Sentry's getClient to return undefined + vi.mocked(Sentry.getClient).mockReturnValue(undefined); + + // Ensure Sentry's captureException and captureMessage methods are mocked + vi.mocked(Sentry.captureException).mockImplementation(vi.fn()); + vi.mocked(Sentry.captureMessage).mockImplementation(vi.fn()); + }); + + it('should not call Sentry.captureException', () => { + const error = new Error('Test error'); + const message = 'Test error message'; + + logger.error(message, error); + + // Verify that captureException was not called + expect(Sentry.captureException).not.toHaveBeenCalled(); + }); + + it('should not call Sentry.captureMessage for info', () => { + const message = 'Test info message'; + + logger.info(message); + + // Verify that captureMessage was not called + expect(Sentry.captureMessage).not.toHaveBeenCalled(); + }); + + it('should not call Sentry.captureMessage for warn', () => { + const message = 'Test warn message'; + + logger.warn(message); + + // Verify that captureMessage was not called + expect(Sentry.captureMessage).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/platforms/access-bridge/test/unit/signing.test.ts b/platforms/access-bridge/test/unit/signing.test.ts new file mode 100644 index 000000000..e8b76584c --- /dev/null +++ b/platforms/access-bridge/test/unit/signing.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; + +import { PassportService } from '../../src/services/passport-service.js'; + +describe('PassportService generateSignedUrl test', () => { + const service = new PassportService(); + + it('should generate a signed URL with the correct token', async () => { + const path = '/path/to/resource'; + const clientHost = 'https://example.com'; + + const result = await service.generateSignedUrl(path, clientHost); + + // Parse the result URL to extract the token + const url = new URL(result); + const token = url.searchParams.get('token'); + + expect(result).toBe(`${clientHost}${path}?token=${token}`); + }); +}); diff --git a/platforms/access-bridge/test/vitest.setup.ts b/platforms/access-bridge/test/vitest.setup.ts new file mode 100644 index 000000000..58e772880 --- /dev/null +++ b/platforms/access-bridge/test/vitest.setup.ts @@ -0,0 +1,20 @@ +import { vi } from 'vitest'; +import * as Sentry from '@sentry/node'; + +// Mock sentry lib +vi.mock('@sentry/node', async (importOriginal) => { + const actual = (await importOriginal()) as typeof Sentry; + + return { + ...actual, + init: vi.fn(), + captureException: vi.fn(), + captureMessage: vi.fn(), + getClient: vi.fn(), + }; +}); + +beforeEach(() => { + // Clear all mocks + vi.clearAllMocks(); +}); diff --git a/platforms/access-bridge/tsconfig.json b/platforms/access-bridge/tsconfig.json new file mode 100644 index 000000000..9f3252104 --- /dev/null +++ b/platforms/access-bridge/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "build", + "incremental": true, + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "node", + "strict": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "noUnusedLocals": true, + "types": ["vitest/globals"], + }, + "include": ["test/**/*", "src/**/*", "instrument.mjs"], + "exclude": ["node_modules", "build"], +} diff --git a/platforms/access-bridge/vite.config.ts b/platforms/access-bridge/vite.config.ts new file mode 100644 index 000000000..c671b75ca --- /dev/null +++ b/platforms/access-bridge/vite.config.ts @@ -0,0 +1,58 @@ +import { defineConfig, loadEnv, PluginOption } from 'vite'; +import { VitePluginNode } from 'vite-plugin-node'; +import type { ConfigEnv, UserConfigExport } from 'vitest/config'; +import { sentryVitePlugin } from '@sentry/vite-plugin'; + +export default ({ mode, command }: ConfigEnv): UserConfigExport => { + const envPrefix = 'APP_'; + const env = loadEnv(mode, process.cwd(), envPrefix); + + // Shorten default mode names to dev / prod + mode = mode === 'development' ? 'dev' : mode; + mode = mode === 'production' ? 'prod' : mode; + + // Ensure builds are always production type + if (command === 'build') { + process.env.NODE_ENV = 'production'; + } + + // Define the initial plugins array with the Vite Node plugin for Node.js support + const plugins: PluginOption[] = [ + VitePluginNode({ + adapter: 'express', + appPath: 'src/main.ts', + tsCompiler: 'esbuild', + }), + ]; + + // Conditionally add the Sentry plugin based on the mode and presence of Sentry-related env variables + if (mode === 'prod' && env.APP_SENTRY_DSN && env.APP_SENTRY_AUTH_TOKEN) { + plugins.push( + sentryVitePlugin({ + authToken: env.APP_SENTRY_AUTH_TOKEN, + org: env.APP_SENTRY_ORG_NAME, + project: env.APP_SENTRY_PROJ_NAME, + }) + ); + } + + return defineConfig({ + server: { + port: parseInt(env.APP_BIND_PORT), + }, + envPrefix, + plugins, + build: { + outDir: 'build', + sourcemap: true, + }, + test: { + globals: true, + include: ['**/*.test.ts'], + setupFiles: 'test/vitest.setup.ts', + chaiConfig: { + truncateThreshold: 1000, + }, + }, + }); +}; diff --git a/yarn.lock b/yarn.lock index 2671a7a1d..295f2bb20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -65,6 +65,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== +"@babel/compat-data@^7.25.2": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.4.tgz#7d2a80ce229890edcf4cc259d4d696cb4dae2fcb" + integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== + "@babel/core@^7.11.1", "@babel/core@^7.13.16", "@babel/core@^7.20.0", "@babel/core@^7.21.3", "@babel/core@^7.24.4", "@babel/core@^7.24.5", "@babel/core@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" @@ -86,6 +91,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.18.5": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.20.0", "@babel/generator@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" @@ -96,6 +122,16 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" +"@babel/generator@^7.25.0", "@babel/generator@^7.25.4": + version "7.25.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.5.tgz#b31cf05b3fe8c32d206b6dad03bb0aacbde73450" + integrity sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w== + dependencies: + "@babel/types" "^7.25.4" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" @@ -122,6 +158,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== + dependencies: + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" @@ -206,6 +253,16 @@ "@babel/helper-split-export-declaration" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" +"@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" + "@babel/helper-optimise-call-expression@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" @@ -264,6 +321,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" @@ -274,6 +336,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== +"@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + "@babel/helper-wrap-function@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" @@ -292,6 +359,14 @@ "@babel/template" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/helpers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" + integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== + dependencies: + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.0" + "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" @@ -307,6 +382,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== +"@babel/parser@^7.25.0", "@babel/parser@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.4.tgz#af4f2df7d02440286b7de57b1c21acfb2a6f257a" + integrity sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA== + dependencies: + "@babel/types" "^7.25.4" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" @@ -1204,6 +1286,15 @@ "@babel/parser" "^7.24.7" "@babel/types" "^7.24.7" +"@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" + "@babel/traverse@^7.20.0", "@babel/traverse@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" @@ -1220,6 +1311,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.25.2": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.4.tgz#648678046990f2957407e3086e97044f13c3e18e" + integrity sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.4" + "@babel/parser" "^7.25.4" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.4" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.24.7", "@babel/types@^7.4.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" @@ -1229,6 +1333,15 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.4": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.4.tgz#6bcb46c72fdf1012a209d016c07f769e10adcb5f" + integrity sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2242,6 +2355,270 @@ resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323" integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw== +"@opentelemetry/api-logs@0.52.1": + version "0.52.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc" + integrity sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A== + dependencies: + "@opentelemetry/api" "^1.0.0" + +"@opentelemetry/api-logs@0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz#c478cbd8120ec2547b64edfa03a552cfe42170be" + integrity sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw== + dependencies: + "@opentelemetry/api" "^1.0.0" + +"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + +"@opentelemetry/context-async-hooks@^1.25.1": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz#fa92f722cf685685334bba95f258d3ef9fce60f6" + integrity sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg== + +"@opentelemetry/core@1.26.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.26.0.tgz#7d84265aaa850ed0ca5813f97d831155be42b328" + integrity sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ== + dependencies: + "@opentelemetry/semantic-conventions" "1.27.0" + +"@opentelemetry/instrumentation-connect@0.39.0": + version "0.39.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.39.0.tgz#32bdbaac464cba061c95df6c850ee81efdd86f8b" + integrity sha512-pGBiKevLq7NNglMgqzmeKczF4XQMTOUOTkK8afRHMZMnrK3fcETyTH7lVaSozwiOM3Ws+SuEmXZT7DYrrhxGlg== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@types/connect" "3.4.36" + +"@opentelemetry/instrumentation-express@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.42.0.tgz#279f195aa66baee2b98623a16666c6229c8e7564" + integrity sha512-YNcy7ZfGnLsVEqGXQPT+S0G1AE46N21ORY7i7yUQyfhGAL4RBjnZUqefMI0NwqIl6nGbr1IpF0rZGoN8Q7x12Q== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-fastify@0.39.0": + version "0.39.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.39.0.tgz#96a040e4944daf77c53a8fe5a128bc3b2568e4aa" + integrity sha512-SS9uSlKcsWZabhBp2szErkeuuBDgxOUlllwkS92dVaWRnMmwysPhcEgHKB8rUe3BHg/GnZC1eo1hbTZv4YhfoA== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-fs@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.15.0.tgz#41658507860f39fee5209bca961cea8d24ca2a83" + integrity sha512-JWVKdNLpu1skqZQA//jKOcKdJC66TWKqa2FUFq70rKohvaSq47pmXlnabNO+B/BvLfmidfiaN35XakT5RyMl2Q== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + +"@opentelemetry/instrumentation-generic-pool@0.39.0": + version "0.39.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz#2b9af16ad82d5cbe67125c0125753cecd162a728" + integrity sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + +"@opentelemetry/instrumentation-graphql@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.43.0.tgz#71bb94ea775c70dbd388c739b397ec1418f3f170" + integrity sha512-aI3YMmC2McGd8KW5du1a2gBA0iOMOGLqg4s9YjzwbjFwjlmMNFSK1P3AIg374GWg823RPUGfVTIgZ/juk9CVOA== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + +"@opentelemetry/instrumentation-hapi@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz#de8711907256d8fae1b5faf71fc825cef4a7ddbb" + integrity sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-http@0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz#0d806adf1b3aba036bc46e16162e3c0dbb8a6b60" + integrity sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ== + dependencies: + "@opentelemetry/core" "1.26.0" + "@opentelemetry/instrumentation" "0.53.0" + "@opentelemetry/semantic-conventions" "1.27.0" + semver "^7.5.2" + +"@opentelemetry/instrumentation-ioredis@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz#dbadabaeefc4cb47c406f878444f1bcac774fa89" + integrity sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/redis-common" "^0.36.2" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-kafkajs@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz#6687bce4dac8b90ef8ccbf1b662d5d1e95a34414" + integrity sha512-UnkZueYK1ise8FXQeKlpBd7YYUtC7mM8J0wzUSccEfc/G8UqHQqAzIyYCUOUPUKp8GsjLnWOOK/3hJc4owb7Jg== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-koa@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz#963fd192a1b5f6cbae5dabf4ec82e3105cbb23b1" + integrity sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-mongodb@0.47.0": + version "0.47.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz#f8107d878281433905e717f223fb4c0f10356a7b" + integrity sha512-yqyXRx2SulEURjgOQyJzhCECSh5i1uM49NUaq9TqLd6fA7g26OahyJfsr9NE38HFqGRHpi4loyrnfYGdrsoVjQ== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/sdk-metrics" "^1.9.1" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-mongoose@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz#375afd21adfcd897a8f521c1ffd2d91e6a428705" + integrity sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-mysql2@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz#6377b6e2d2487fd88e1d79aa03658db6c8d51651" + integrity sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/sql-common" "^0.40.1" + +"@opentelemetry/instrumentation-mysql@0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz#2d50691ead5219774bd36d66c35d5b4681485dd7" + integrity sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@types/mysql" "2.15.26" + +"@opentelemetry/instrumentation-nestjs-core@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz#2c0e6405b56caaec32747d55c57ff9a034668ea8" + integrity sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-pg@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz#1e97a0aeb2dca068ee23ce75884a0a0063a7ce3f" + integrity sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/sql-common" "^0.40.1" + "@types/pg" "8.6.1" + "@types/pg-pool" "2.0.6" + +"@opentelemetry/instrumentation-redis-4@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz#fc01104cfe884c7546385eaae03c57a47edd19d1" + integrity sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/redis-common" "^0.36.2" + "@opentelemetry/semantic-conventions" "^1.27.0" + +"@opentelemetry/instrumentation-undici@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz#9436ee155c8dcb0b760b66947c0e0f347688a5ef" + integrity sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.53.0" + +"@opentelemetry/instrumentation@0.53.0", "@opentelemetry/instrumentation@^0.53.0": + version "0.53.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d" + integrity sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A== + dependencies: + "@opentelemetry/api-logs" "0.53.0" + "@types/shimmer" "^1.2.0" + import-in-the-middle "^1.8.1" + require-in-the-middle "^7.1.1" + semver "^7.5.2" + shimmer "^1.2.1" + +"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0": + version "0.52.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" + integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== + dependencies: + "@opentelemetry/api-logs" "0.52.1" + "@types/shimmer" "^1.0.2" + import-in-the-middle "^1.8.1" + require-in-the-middle "^7.1.1" + semver "^7.5.2" + shimmer "^1.2.1" + +"@opentelemetry/redis-common@^0.36.2": + version "0.36.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz#906ac8e4d804d4109f3ebd5c224ac988276fdc47" + integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g== + +"@opentelemetry/resources@1.26.0", "@opentelemetry/resources@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.26.0.tgz#da4c7366018bd8add1f3aa9c91c6ac59fd503cef" + integrity sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw== + dependencies: + "@opentelemetry/core" "1.26.0" + "@opentelemetry/semantic-conventions" "1.27.0" + +"@opentelemetry/sdk-metrics@^1.9.1": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz#37bb0afb1d4447f50aab9cdd05db6f2d8b86103e" + integrity sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ== + dependencies: + "@opentelemetry/core" "1.26.0" + "@opentelemetry/resources" "1.26.0" + +"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz#0c913bc6d2cfafd901de330e4540952269ae579c" + integrity sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw== + dependencies: + "@opentelemetry/core" "1.26.0" + "@opentelemetry/resources" "1.26.0" + "@opentelemetry/semantic-conventions" "1.27.0" + +"@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.27.0": + version "1.27.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" + integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== + +"@opentelemetry/sql-common@^0.40.1": + version "0.40.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz#93fbc48d8017449f5b3c3274f2268a08af2b83b6" + integrity sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg== + dependencies: + "@opentelemetry/core" "^1.1.0" + "@paulirish/trace_engine@^0.0.23": version "0.0.23" resolved "https://registry.yarnpkg.com/@paulirish/trace_engine/-/trace_engine-0.0.23.tgz#b3eec22421ee562837b371ddcd4659483837ec92" @@ -2257,6 +2634,15 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-0.5.0.tgz#b21510597fd601e5d7c95008b76bf0d254ebfd31" integrity sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw== +"@prisma/instrumentation@5.19.1": + version "5.19.1" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.19.1.tgz#146319cf85f22b7a43296f0f40cfeac55516e66e" + integrity sha512-VLnzMQq7CWroL5AeaW0Py2huiNKeoMfCH3SUxstdzPrlWQi6UQ9UrfcbUkNHlVFqOMacqy8X/8YtE0kuKDpD9w== + dependencies: + "@opentelemetry/api" "^1.8" + "@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0" + "@opentelemetry/sdk-trace-base" "^1.22" + "@promptbook/utils@0.58.0": version "0.58.0" resolved "https://registry.yarnpkg.com/@promptbook/utils/-/utils-0.58.0.tgz#f85439d56e5c0593b81a1003c9dd0769f89ba76f" @@ -2665,7 +3051,7 @@ estree-walker "^1.0.1" picomatch "^2.2.2" -"@rollup/pluginutils@^4.2.0", "@rollup/pluginutils@^4.2.1": +"@rollup/pluginutils@^4.1.1", "@rollup/pluginutils@^4.2.0", "@rollup/pluginutils@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== @@ -2762,6 +3148,79 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== +"@sentry/babel-plugin-component-annotate@2.22.2": + version "2.22.2" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.2.tgz#f4a1ddea4bcac06584a6cec9a43ec088cbb6caaf" + integrity sha512-6kFAHGcs0npIC4HTt4ULs8uOfEucvMI7VW4hoyk17jhRaW8CbxzxfWCfIeRbDkE8pYwnARaq83tu025Hrk2zgA== + +"@sentry/bundler-plugin-core@2.22.2": + version "2.22.2" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.2.tgz#bd418541245c5167a439d4e28a84096deb20c512" + integrity sha512-TwEEW4FeEJ5Mamp4fGnktfVjzN77KAW0xFQsEPuxZtOAPG17zX/PGvdyRX/TE1jkZWhTzqUDIdgzqlNLjyEnUw== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "2.22.2" + "@sentry/cli" "^2.33.1" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + +"@sentry/cli-darwin@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.33.1.tgz#e4eb1dd01ee3ce2788025426b860ccc63759589c" + integrity sha512-+4/VIx/E1L2hChj5nGf5MHyEPHUNHJ/HoG5RY+B+vyEutGily1c1+DM2bum7RbD0xs6wKLIyup5F02guzSzG8A== + +"@sentry/cli-linux-arm64@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.1.tgz#9ea1718c21ef32ca83b0852ca29fb461fd26d25a" + integrity sha512-DbGV56PRKOLsAZJX27Jt2uZ11QfQEMmWB4cIvxkKcFVE+LJP4MVA+MGGRUL6p+Bs1R9ZUuGbpKGtj0JiG6CoXw== + +"@sentry/cli-linux-arm@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.1.tgz#e8a1dca4d008dd6a72ab5935304c104e98e2901c" + integrity sha512-zbxEvQju+tgNvzTOt635le4kS/Fbm2XC2RtYbCTs034Vb8xjrAxLnK0z1bQnStUV8BkeBHtsNVrG+NSQDym2wg== + +"@sentry/cli-linux-i686@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.1.tgz#f1fe8dd4d6dde0812a94fba31de8054ddfb7284a" + integrity sha512-g2LS4oPXkPWOfKWukKzYp4FnXVRRSwBxhuQ9eSw2peeb58ZIObr4YKGOA/8HJRGkooBJIKGaAR2mH2Pk1TKaiA== + +"@sentry/cli-linux-x64@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.1.tgz#6e086675356a9eb79731bf9e447d078bae1b5adf" + integrity sha512-IV3dcYV/ZcvO+VGu9U6kuxSdbsV2kzxaBwWUQxtzxJ+cOa7J8Hn1t0koKGtU53JVZNBa06qJWIcqgl4/pCuKIg== + +"@sentry/cli-win32-i686@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.1.tgz#0e6b36c4a2f5f6e85a59247a123d276b3ef10f1a" + integrity sha512-F7cJySvkpzIu7fnLKNHYwBzZYYwlhoDbAUnaFX0UZCN+5DNp/5LwTp37a5TWOsmCaHMZT4i9IO4SIsnNw16/zQ== + +"@sentry/cli-win32-x64@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.1.tgz#2d00b38a2dd9f3355df91825582ada3ea0034e86" + integrity sha512-8VyRoJqtb2uQ8/bFRKNuACYZt7r+Xx0k2wXRGTyH05lCjAiVIXn7DiS2BxHFty7M1QEWUCMNsb/UC/x/Cu2wuA== + +"@sentry/cli@^2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.33.1.tgz#cfbdffdd896b05b92a659baf435b5607037af928" + integrity sha512-dUlZ4EFh98VFRPJ+f6OW3JEYQ7VvqGNMa0AMcmvk07ePNeK/GicAWmSQE4ZfJTTl80ul6HZw1kY01fGQOQlVRA== + dependencies: + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + optionalDependencies: + "@sentry/cli-darwin" "2.33.1" + "@sentry/cli-linux-arm" "2.33.1" + "@sentry/cli-linux-arm64" "2.33.1" + "@sentry/cli-linux-i686" "2.33.1" + "@sentry/cli-linux-x64" "2.33.1" + "@sentry/cli-win32-i686" "2.33.1" + "@sentry/cli-win32-x64" "2.33.1" + "@sentry/core@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785" @@ -2773,6 +3232,14 @@ "@sentry/utils" "6.19.7" tslib "^1.9.3" +"@sentry/core@8.30.0": + version "8.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.30.0.tgz#f929e42e9a537bfa3eb6024082714e9ab98d822b" + integrity sha512-CJ/FuWLw0QEKGKXGL/nm9eaOdajEcmPekLuHAuOCxID7N07R9l9laz3vFbAkUZ97GGDv3sYrJZgywfY3Moropg== + dependencies: + "@sentry/types" "8.30.0" + "@sentry/utils" "8.30.0" + "@sentry/hub@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.7.tgz#58ad7776bbd31e9596a8ec46365b45cd8b9cfd11" @@ -2791,6 +3258,44 @@ "@sentry/types" "6.19.7" tslib "^1.9.3" +"@sentry/node@8.30.0", "@sentry/node@^8.26.0": + version "8.30.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.30.0.tgz#730461be3a3382ab17e2c3f95c08e4e85a207429" + integrity sha512-Tog0Ag7sU3lNj4cPUZy1KRJXyYXZlWiwlk34KYNNxAk0vDiK6W0bF8mvS+aaUukgb7FO5A0eu9l+VApdBJOr3Q== + dependencies: + "@opentelemetry/api" "^1.9.0" + "@opentelemetry/context-async-hooks" "^1.25.1" + "@opentelemetry/core" "^1.25.1" + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation-connect" "0.39.0" + "@opentelemetry/instrumentation-express" "0.42.0" + "@opentelemetry/instrumentation-fastify" "0.39.0" + "@opentelemetry/instrumentation-fs" "0.15.0" + "@opentelemetry/instrumentation-generic-pool" "0.39.0" + "@opentelemetry/instrumentation-graphql" "0.43.0" + "@opentelemetry/instrumentation-hapi" "0.41.0" + "@opentelemetry/instrumentation-http" "0.53.0" + "@opentelemetry/instrumentation-ioredis" "0.43.0" + "@opentelemetry/instrumentation-kafkajs" "0.3.0" + "@opentelemetry/instrumentation-koa" "0.43.0" + "@opentelemetry/instrumentation-mongodb" "0.47.0" + "@opentelemetry/instrumentation-mongoose" "0.42.0" + "@opentelemetry/instrumentation-mysql" "0.41.0" + "@opentelemetry/instrumentation-mysql2" "0.41.0" + "@opentelemetry/instrumentation-nestjs-core" "0.40.0" + "@opentelemetry/instrumentation-pg" "0.44.0" + "@opentelemetry/instrumentation-redis-4" "0.42.0" + "@opentelemetry/instrumentation-undici" "0.6.0" + "@opentelemetry/resources" "^1.26.0" + "@opentelemetry/sdk-trace-base" "^1.26.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@prisma/instrumentation" "5.19.1" + "@sentry/core" "8.30.0" + "@sentry/opentelemetry" "8.30.0" + "@sentry/types" "8.30.0" + "@sentry/utils" "8.30.0" + import-in-the-middle "^1.11.0" + "@sentry/node@^6.17.4": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.19.7.tgz#32963b36b48daebbd559e6f13b1deb2415448592" @@ -2805,11 +3310,37 @@ lru_map "^0.3.3" tslib "^1.9.3" +"@sentry/opentelemetry@8.30.0": + version "8.30.0" + resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.30.0.tgz#b80417d44e4d20f03a7ecf5173a8a4ed4f317a57" + integrity sha512-6mCIP2zvxAiEsNEoF8kv+UUD4XGWSKJU6RY5BF1U26HLitXv1fNPtzaTR96Ehv9h0zktjLfqfpVUZ7DGkdBvLA== + dependencies: + "@sentry/core" "8.30.0" + "@sentry/types" "8.30.0" + "@sentry/utils" "8.30.0" + +"@sentry/profiling-node@^8.26.0": + version "8.30.0" + resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-8.30.0.tgz#f30a7d0b2e3ffe7dc65993b33574414b6b590111" + integrity sha512-HKjIjHRtgEpUjO8LXadWEA/G8fwZ4Ej/KVfUK3tdZYimY6ISwzLQj/PFIoROOdqqWEw5z1mcF89AgIhGoKaH+Q== + dependencies: + "@sentry/core" "8.30.0" + "@sentry/node" "8.30.0" + "@sentry/types" "8.30.0" + "@sentry/utils" "8.30.0" + detect-libc "^2.0.2" + node-abi "^3.61.0" + "@sentry/types@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7" integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg== +"@sentry/types@8.30.0": + version "8.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.30.0.tgz#5f5011f5b16bafd30a039ca5e8c337e948c703fb" + integrity sha512-kgWW2BCjBmVlSQRG32GonHEVyeDbys74xf9mLPvynwHTgw3+NUlNAlEdu05xnb2ow4bCTHfbkS5G1zRgyv5k4Q== + "@sentry/utils@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79" @@ -2818,6 +3349,21 @@ "@sentry/types" "6.19.7" tslib "^1.9.3" +"@sentry/utils@8.30.0": + version "8.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.30.0.tgz#2343dd8593ea83890b3e0d792ed3fa257955a26b" + integrity sha512-wZxU2HWlzsnu8214Xy7S7cRIuD6h8Z5DnnkojJfX0i0NLooepZQk2824el1Q13AakLb7/S8CHSHXOMnCtoSduw== + dependencies: + "@sentry/types" "8.30.0" + +"@sentry/vite-plugin@^2.22.2": + version "2.22.2" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.2.tgz#9d0ca7b3dc29616457c3d0a22e4885506e32184d" + integrity sha512-LJSNTw75UJq77v2jCan8cRh0w1u6W30jxQsbqF7YyyhhfjPTyFUXYday9RDDe84qDEnspbZmgeTSWTvaTGsBRg== + dependencies: + "@sentry/bundler-plugin-core" "2.22.2" + unplugin "1.0.1" + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -3066,6 +3612,35 @@ dependencies: "@babel/types" "^7.20.7" +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/connect@3.4.36": + version "3.4.36" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab" + integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + "@types/dompurify@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" @@ -3091,11 +3666,36 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/googlepay@0.7.6": version "0.7.6" resolved "https://registry.yarnpkg.com/@types/googlepay/-/googlepay-0.7.6.tgz#ba444ad8b2945e70f873673b8f5371745b8cfe37" integrity sha512-5003wG+qvf4Ktf1hC9IJuRakNzQov00+Xf09pAWGJLpdOjUrq0SSLCpXX7pwSeTG9r5hrdzq1iFyZcW7WVyr4g== +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + "@types/ini@^1.3.34": version "1.3.34" resolved "https://registry.yarnpkg.com/@types/ini/-/ini-1.3.34.tgz#99a69ecfccdfc3f6e91b411d4208aaa3c4cc9685" @@ -3130,6 +3730,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== + dependencies: + "@types/node" "*" + "@types/jwplayer@^8.31.1": version "8.31.1" resolved "https://registry.yarnpkg.com/@types/jwplayer/-/jwplayer-8.31.1.tgz#c70c6e69981880a635ff13362a0ec490134a3242" @@ -3157,6 +3764,11 @@ resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.3.2.tgz#e2e0ad02ebf5626bd215c5bae2aff6aff0ce9eac" integrity sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -3167,6 +3779,13 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== +"@types/mysql@2.15.26": + version "2.15.26" + resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.26.tgz#f0de1484b9e2354d587e7d2bd17a873cc8300836" + integrity sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ== + dependencies: + "@types/node" "*" + "@types/node@*": version "20.14.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.5.tgz#fe35e3022ebe58b8f201580eb24e1fcfc0f2487d" @@ -3179,6 +3798,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== +"@types/node@>=8.1.0": + version "22.5.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" + integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA== + dependencies: + undici-types "~6.19.2" + "@types/node@^18.19.37": version "18.19.50" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.50.tgz#8652b34ee7c0e7e2004b3f08192281808d41bf5a" @@ -3193,6 +3819,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.14.10": + version "20.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" + integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -3208,6 +3841,31 @@ resolved "https://registry.yarnpkg.com/@types/payment/-/payment-2.1.7.tgz#02e6cb0cf8b576e72a35477111c1d754d3ac3d1f" integrity sha512-J9BCWMJ+S2PBCSVTvdE+/ziiV73Cu3Wu6WQCC6D+1AO8vphGaQUM6hOy+UaKo84md5mrMcn7P9G/TmD9Y2922A== +"@types/pg-pool@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.6.tgz#1376d9dc5aec4bb2ec67ce28d7e9858227403c77" + integrity sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ== + dependencies: + "@types/pg" "*" + +"@types/pg@*": + version "8.11.9" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.9.tgz#3e92f7edbe4df9de9397f5308d7fe80c31faefe8" + integrity sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^4.0.1" + +"@types/pg@8.6.1": + version "8.6.1" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.6.1.tgz#099450b8dc977e8197a44f5229cedef95c8747f9" + integrity sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w== + dependencies: + "@types/node" "*" + pg-protocol "*" + pg-types "^2.2.0" + "@types/postcss-modules-local-by-default@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.2.tgz#8fee7513dd1558d74713d817c183a33a6dc583f9" @@ -3227,6 +3885,16 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + "@types/react-dom@^18.0.0", "@types/react-dom@^18.3.0": version "18.3.0" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" @@ -3268,6 +3936,28 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/shimmer@^1.0.2", "@types/shimmer@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded" + integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg== + "@types/stack-utils@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" @@ -3588,7 +4278,7 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7: +accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -3596,6 +4286,11 @@ accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.34" negotiator "0.6.3" +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -3618,6 +4313,11 @@ acorn@^8.11.0, acorn@^8.11.3, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== +acorn@^8.8.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -3809,6 +4509,11 @@ array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -4162,6 +4867,24 @@ bo-selector@0.0.10: resolved "https://registry.yarnpkg.com/bo-selector/-/bo-selector-0.0.10.tgz#9816dcb00adf374ea87941a863b2acfc026afa3e" integrity sha512-Drm8W3MFLNhzHTXG93g8ll7wBlmiRr5C9W8R0sbsNQp/8h1IoPnzDH4dEQuJx8VaNq02io2ZfFnzKC1s64xRJg== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -4265,6 +4988,16 @@ browserslist@^4.22.2, browserslist@^4.23.0: node-releases "^2.0.14" update-browserslist-db "^1.0.16" +browserslist@^4.23.1: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -4277,6 +5010,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -4347,6 +5085,11 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + cac@^6.7.14: version "6.7.14" resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" @@ -4434,6 +5177,11 @@ caniuse-lite@^1.0.30001629: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== +caniuse-lite@^1.0.30001646: + version "1.0.30001653" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" + integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== + centra@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/centra/-/centra-2.7.0.tgz#4c8312a58436e8a718302011561db7e6a2b0ec18" @@ -4688,6 +5436,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +cjs-module-lexer@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" + integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== + class-transformer@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" @@ -5082,6 +5835,18 @@ console-grid@^2.2.2: resolved "https://registry.yarnpkg.com/console-grid/-/console-grid-2.2.2.tgz#7b786f6c977b1ae5dcaab21c167f97329ca3fad4" integrity sha512-ohlgXexdDTKLNsZz7DSJuCAwmRc8omSS61txOk39W3NOthgKGr1a1jJpZ5BCQe4PlrwMw01OvPQ1Bl3G7Y/uFg== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + conventional-changelog-angular@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz#a9a9494c28b7165889144fd5b91573c4aa9ca541" @@ -5116,6 +5881,16 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + cookie@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -5150,6 +5925,14 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig-typescript-loader@^4.0.0: version "4.4.0" resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.4.0.tgz#f3feae459ea090f131df5474ce4b1222912319f9" @@ -5436,7 +6219,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~4.3.6: +debug@^4.3.5, debug@~4.3.6: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -5868,7 +6651,7 @@ dotenv-expand@^8.0.2: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e" integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg== -dotenv@^16.0.0, dotenv@^16.4.2: +dotenv@^16.0.0, dotenv@^16.3.1, dotenv@^16.4.2, dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== @@ -5926,6 +6709,13 @@ easy-table@1.2.0: optionalDependencies: wcwidth "^1.0.1" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + edge-paths@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/edge-paths/-/edge-paths-3.0.5.tgz#9a35361d701d9b5dc07f641cebe8da01ede80937" @@ -5978,6 +6768,11 @@ electron-to-chromium@^1.4.796: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.805.tgz#1d526e384c20944a3c68f618f9774edc384c4733" integrity sha512-8W4UJwX/w9T0QSzINJckTKG6CYpAUTqsaWcWIsdud3I1FYJcMgW9QqT1/4CBff/pP/TihWh13OmiyY8neto6vw== +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== + email-addresses@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-5.0.0.tgz#7ae9e7f58eef7d5e3e2c2c2d3ea49b78dc854fa6" @@ -6572,6 +7367,43 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -6753,6 +7585,19 @@ finalhandler@1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -6888,6 +7733,11 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -7219,6 +8069,16 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^9.3.2: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== + dependencies: + fs.realpath "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -7643,6 +8503,13 @@ ico-endec@*: resolved "https://registry.yarnpkg.com/ico-endec/-/ico-endec-0.1.6.tgz#9b320cc3ed0a0c779f54e998a8db49002abd7c6e" integrity sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ== +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -7650,13 +8517,6 @@ iconv-lite@0.6.3, iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -7720,6 +8580,16 @@ import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-in-the-middle@^1.11.0, import-in-the-middle@^1.8.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" + integrity sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q== + dependencies: + acorn "^8.8.2" + acorn-import-attributes "^1.9.5" + cjs-module-lexer "^1.2.2" + module-details-from-path "^1.0.3" + import-lazy@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" @@ -7826,6 +8696,11 @@ ip-address@^9.0.5: jsbn "1.1.0" sprintf-js "^1.1.3" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -8631,6 +9506,22 @@ jsonpointer@^5.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + "jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -8641,6 +9532,23 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + jwt-decode@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" @@ -8931,16 +9839,41 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + lodash.isfunction@^3.0.9: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -8956,6 +9889,11 @@ lodash.mergewith@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" @@ -9119,6 +10057,13 @@ lz-utils@^2.0.2: resolved "https://registry.yarnpkg.com/lz-utils/-/lz-utils-2.0.2.tgz#9ccf1f76400617da5b3f5a05192f5227cffd6881" integrity sha512-i1PJN4hNEevkrvLMqNWCCac1BcB5SRaghywG7HVzWOyVkFOasLCG19ND1sY1F/ZEsM6SnGtoXyBWnmfqOM5r6g== +magic-string@0.30.8: + version "0.30.8" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -9220,6 +10165,11 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -9265,6 +10215,11 @@ meow@^8.0.0, meow@^8.1.2: type-fest "^0.18.0" yargs-parser "^20.2.3" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -9280,6 +10235,11 @@ metaviewport-parser@0.3.0: resolved "https://registry.yarnpkg.com/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz#6af1e99b5eaf250c049e0af1e84143a39750dea6" integrity sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + metro-babel-transformer@0.80.9: version "0.80.9" resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.80.9.tgz#7051ba377b7d2140abd23f4846bbbb1e81fea99b" @@ -9487,7 +10447,7 @@ mime-lite@^1.0.3: resolved "https://registry.yarnpkg.com/mime-lite/-/mime-lite-1.0.3.tgz#778e1880842545f71b223898cdff1c9ddbc995fe" integrity sha512-V85l97zJSTG8FEvmdTlmNYb0UMrVBwvRjw7bWTf/aT6KjFwtz3iTz8D2tuFIp7lwiaO2C5ecnrEmSkkMRCrqVw== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -9562,6 +10522,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" @@ -9588,6 +10555,11 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1. resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" @@ -9661,6 +10633,11 @@ mocha@10.4.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + moment@^2.19.3: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" @@ -9858,6 +10835,13 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-abi@^3.61.0: + version "3.67.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.67.0.tgz#1d159907f18d18e18809dbbb5df47ed2426a08df" + integrity sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw== + dependencies: + semver "^7.3.5" + node-abort-controller@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -9880,7 +10864,7 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.11, node-fetch@^2.6.12: +node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.11, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -9923,6 +10907,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + node-stream-zip@^1.9.1: version "1.15.0" resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" @@ -10023,7 +11012,7 @@ ob1@0.80.9: resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.80.9.tgz#4ae3edd807536097674ff943509089f5d4e0649f" integrity sha512-v9yOxowkZbxWhKOaaTyLjIm1aLy4ebMNcSn4NYJKOAI/Qv+SkfEfszpLr2GIxsccmb2Y2HA9qtsqiIJ80ucpVA== -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -10103,6 +11092,11 @@ oblivious-set@1.4.0: resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.4.0.tgz#1ee7c90f0605bb2a182fbcc8fffbe324d9994b43" integrity sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg== +obuf@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -10487,7 +11481,7 @@ path-posix@^1.0.0: resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" integrity sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA== -path-scurry@^1.11.1: +path-scurry@^1.11.1, path-scurry@^1.6.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -10495,6 +11489,11 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -10545,6 +11544,45 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-numeric@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" + integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== + +pg-protocol@*: + version "1.6.1" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" + integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== + +pg-types@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg-types@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" + integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== + dependencies: + pg-int8 "1.0.1" + pg-numeric "1.0.2" + postgres-array "~3.0.1" + postgres-bytea "~3.0.0" + postgres-date "~2.1.0" + postgres-interval "^3.0.0" + postgres-range "^1.1.1" + phin@^3.7.0: version "3.7.1" resolved "https://registry.yarnpkg.com/phin/-/phin-3.7.1.tgz#bf841da75ee91286691b10e41522a662aa628fd6" @@ -10759,6 +11797,55 @@ postcss@^8.0.0, postcss@^8.4.28, postcss@^8.4.32, postcss@^8.4.35, postcss@^8.4. picocolors "^1.0.0" source-map-js "^1.2.0" +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-array@~3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" + integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-bytea@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" + integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== + dependencies: + obuf "~1.1.2" + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-date@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" + integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +postgres-interval@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" + integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== + +postgres-range@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + preact@10.13.2: version "10.13.2" resolved "https://registry.yarnpkg.com/preact/-/preact-10.13.2.tgz#2c40c73d57248b57234c4ae6cd9ab9d8186ebc0a" @@ -10847,7 +11934,7 @@ process-nextick-args@^2.0.1, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@2.0.3: +progress@2.0.3, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -10913,6 +12000,14 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + proxy-agent@6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.3.1.tgz#40e7b230552cf44fd23ffaf7c59024b692612687" @@ -11002,6 +12097,20 @@ qj@~2.0.0: resolved "https://registry.yarnpkg.com/qj/-/qj-2.0.0.tgz#054dd3b75ce018728d23a0605f030dfda0bd158a" integrity sha512-8466vlnAF/piI42tzMBUfhaAWn2yBNPOLSSbA2YBlEh+S8CxBXbAO1AwuDReGKYX6LlsK19wBL9cpXZGlgsXxA== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@^6.11.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@^6.9.4: version "6.12.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" @@ -11084,6 +12193,16 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -11509,6 +12628,15 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^7.1.1: + version "7.4.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz#606977820d4b5f9be75e5a108ce34cfed25b3bb4" + integrity sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ== + dependencies: + debug "^4.3.5" + module-details-from-path "^1.0.3" + resolve "^1.22.8" + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -11558,7 +12686,7 @@ resolve-options@^2.0.0: dependencies: value-or-function "^4.0.0" -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.4: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22.8: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -11756,7 +12884,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -11908,7 +13036,7 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -serve-static@^1.13.1: +serve-static@1.15.0, serve-static@^1.13.1: version "1.15.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== @@ -12038,6 +13166,11 @@ shell-quote@^1.6.1, shell-quote@^1.7.2, shell-quote@^1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +shimmer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -12591,6 +13724,14 @@ strip-outer@^1.0.1: dependencies: escape-string-regexp "^1.0.2" +stripe@^16.8.0: + version "16.8.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-16.8.0.tgz#1433e21c5f7505d270f5bc5228d08a6b63a43546" + integrity sha512-6rOIcGOkxcc29jvhEyOYmpPFilekOBV+7vpemAoIAfbtCRW1yxzdDGM0/0vyekHglLL+wqGpP5ldrhO3dJ2JEQ== + dependencies: + "@types/node" ">=8.1.0" + qs "^6.11.0" + strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" @@ -13229,6 +14370,14 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" @@ -13322,6 +14471,11 @@ typescript@^5.1.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== +typescript@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" + integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== + ua-parser-js@^1.0.37: version "1.0.38" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2" @@ -13372,6 +14526,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -13430,11 +14589,21 @@ unload@2.4.1: resolved "https://registry.yarnpkg.com/unload/-/unload-2.4.1.tgz#b0c5b7fb44e17fcbf50dcb8fb53929c59dd226a5" integrity sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw== -unpipe@~1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unplugin@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" + integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA== + dependencies: + acorn "^8.8.1" + chokidar "^3.5.3" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.5.0" + upath@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" @@ -13448,6 +14617,14 @@ update-browserslist-db@^1.0.16: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -13540,7 +14717,7 @@ value-or-function@^4.0.0: resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-4.0.0.tgz#70836b6a876a010dc3a2b884e7902e9db064378d" integrity sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -13642,6 +14819,15 @@ vite-plugin-html@^3.2.2: node-html-parser "^5.3.3" pathe "^0.2.0" +vite-plugin-node@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vite-plugin-node/-/vite-plugin-node-3.1.0.tgz#e8f695f1634475f2e816ff8a734e6f844e6fd250" + integrity sha512-LN9byKedZaokEWcH+DTdeV7YLN34dWH7Em2ldHZ77oVGvC/uxHnK43AGPrV9+9CoOh1x9X3Yc55vW/OQYssFDQ== + dependencies: + "@rollup/pluginutils" "^4.1.1" + chalk "^4.1.2" + debug "^4.3.2" + vite-plugin-pwa@^0.19.8: version "0.19.8" resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-0.19.8.tgz#f7be200a4426207358aef807b4a6e1ecbc14d345" @@ -13805,6 +14991,16 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack-virtual-modules@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" + integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== + websocket-stream@^5.5.2: version "5.5.2" resolved "https://registry.yarnpkg.com/websocket-stream/-/websocket-stream-5.5.2.tgz#49d87083d96839f0648f5513bbddd581f496b8a2" From 0e51bc44288fb1ab2cd55dc8bdeaec8233819043 Mon Sep 17 00:00:00 2001 From: Kire Mitrov Date: Mon, 23 Sep 2024 13:07:09 +0200 Subject: [PATCH 08/16] fix: update plan types and access bridge port for the unit test workflow (#619) * fix: update plan types and access bridge port for the unit test workflow * fix: add 3001 as a port for access bridge tests --- .github/workflows/test-unit-snapshot.yml | 2 +- packages/common/types/plans.ts | 10 +++- platforms/access-bridge/test/fixtures.ts | 74 +++++++++++++++++------- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test-unit-snapshot.yml b/.github/workflows/test-unit-snapshot.yml index 8d87ddbcf..460389c23 100644 --- a/.github/workflows/test-unit-snapshot.yml +++ b/.github/workflows/test-unit-snapshot.yml @@ -26,6 +26,6 @@ jobs: APP_API_SECRET: dummy_secret APP_STRIPE_SECRET: dummy_stripe_secret APP_BIND_ADDR: localhost - APP_BIND_PORT: 8080 + APP_BIND_PORT: 3001 APP_ACCESS_CONTROL_API_HOST: https://test-cdn.jwplayer.com APP_SIMS_API_HOST: https://test-sims.jwplayer.com diff --git a/packages/common/types/plans.ts b/packages/common/types/plans.ts index 331bccfac..400703850 100644 --- a/packages/common/types/plans.ts +++ b/packages/common/types/plans.ts @@ -1,5 +1,9 @@ type AccessOptions = { drm_policy_id: string; + include_tags: string[] | null; + exclude_tags: string[] | null; + include_custom_params: string[] | null; + exclude_custom_params: string[] | null; }; type PlanExternalProviders = { @@ -15,10 +19,12 @@ export type AccessControlPlan = { export type Plan = { id: string; + original_id: number; exp: number; - access_model: 'free' | 'freeauth' | 'svod'; - access: AccessOptions; metadata: { + name: string; + access: AccessOptions; + access_model: 'free' | 'freeauth' | 'svod'; external_providers: PlanExternalProviders; }; }; diff --git a/platforms/access-bridge/test/fixtures.ts b/platforms/access-bridge/test/fixtures.ts index 503850ae8..38a72750e 100644 --- a/platforms/access-bridge/test/fixtures.ts +++ b/platforms/access-bridge/test/fixtures.ts @@ -44,24 +44,40 @@ export const VIEWER: Viewer = { }; // Plan mock creation function -const createMockPlan = ({ id, exp, access_model, access, metadata }: Plan): Plan => ({ +const createMockPlan = ({ id, original_id, exp, metadata }: Plan): Plan => ({ id, + original_id, exp, - access_model, - access, - metadata, + metadata: { + name: metadata.name || '', + access: { + drm_policy_id: metadata.access.drm_policy_id, + include_tags: metadata.access.include_tags || [], + exclude_tags: metadata.access.exclude_tags || [], + include_custom_params: metadata.access.include_custom_params || [], + exclude_custom_params: metadata.access.exclude_custom_params || [], + }, + access_model: metadata.access_model, + external_providers: metadata.external_providers || {}, + }, }); export const PLANS = { VALID: [ createMockPlan({ id: 'plan1234', + original_id: 123456, exp: FUTURE_EXPIRY, - access_model: 'svod', - access: { - drm_policy_id: 'drm_policy_123', - }, metadata: { + name: 'Test plan', + access: { + drm_policy_id: 'drm_policy_123', + include_tags: [], + exclude_tags: [], + include_custom_params: [], + exclude_custom_params: [], + }, + access_model: 'svod', external_providers: { stripe: 'stripe_id', }, @@ -71,12 +87,18 @@ export const PLANS = { FREE: [ createMockPlan({ id: 'free1234', + original_id: 123457, exp: FUTURE_EXPIRY, - access_model: 'free', - access: { - drm_policy_id: 'drm_policy_456', - }, metadata: { + name: 'Free plan', + access: { + drm_policy_id: 'drm_policy_456', + include_tags: [], + exclude_tags: [], + include_custom_params: [], + exclude_custom_params: [], + }, + access_model: 'free', external_providers: {}, }, }), @@ -84,12 +106,18 @@ export const PLANS = { INVALID: [ createMockPlan({ id: 'plan123456', + original_id: 123458, exp: FUTURE_EXPIRY, - access_model: 'svod', - access: { - drm_policy_id: 'drm_policy_789', - }, metadata: { + name: 'Invalid plan', + access: { + drm_policy_id: 'drm_policy_789', + include_tags: [], + exclude_tags: [], + include_custom_params: [], + exclude_custom_params: [], + }, + access_model: 'svod', external_providers: {}, }, }), @@ -97,12 +125,18 @@ export const PLANS = { EXPIRED: [ createMockPlan({ id: 'plan1234', + original_id: 123459, exp: PAST_EXPIRY, - access_model: 'svod', - access: { - drm_policy_id: 'drm_policy_101', - }, metadata: { + name: 'Expired plan', + access: { + drm_policy_id: 'drm_policy_101', + include_tags: [], + exclude_tags: [], + include_custom_params: [], + exclude_custom_params: [], + }, + access_model: 'svod', external_providers: {}, }, }), From 0b3d6072b6d8d0aae710d342c93de1d8bb7b0aa7 Mon Sep 17 00:00:00 2001 From: Kire Mitrov Date: Wed, 25 Sep 2024 12:59:20 +0200 Subject: [PATCH 09/16] [IDM-174] - feat: use passport (#615) * feat(project): use passport service from access bridge * chore: remove unused prop * chore: update import paths * feat(project): remove passport on logout * feat(project): create access service and controller * feat(project): handle avod for anonymous passport generation * chore: update import paths in Access Controller * chore: revert env variables * feat(project): include expires in the passport stored data * feat(project): update init access controller * feat(project): move access bridge env variable to ini * feat(project): add refresh passport mechanism * feat(project): add getEntitledPlans service * feat(project): bind access stuff optionally * chore: naming typo * chore: register imports order * feat(project): add new way of handling media with passport * chore: nit in webapp.dev.ini * fix: add Mock for Access controller in cinema test * fix: make language as optional, to fix lint * feat(project): move entitled plans to the account controller * fix: add 0 as a port for access bridge tests * chore: revert port for tests * fix: query enable condition --- .../src/controllers/AccessController.ts | 178 ++++++++++++++++++ .../src/controllers/AccountController.ts | 36 +++- .../common/src/controllers/AppController.ts | 13 ++ packages/common/src/env.ts | 3 + .../functions/getApiAccessBridgeUrl.ts | 12 ++ packages/common/src/modules/register.ts | 10 +- packages/common/src/modules/types.ts | 2 + packages/common/src/services/AccessService.ts | 59 ++++++ packages/common/src/services/ApiService.ts | 31 ++- .../src/services/JWPEntitlementService.ts | 14 ++ .../common/src/services/SettingsService.ts | 1 + packages/common/src/stores/AccessStore.ts | 9 + packages/common/src/stores/AccountStore.ts | 3 + packages/common/src/utils/sources.ts | 45 ++++- packages/common/types/access.ts | 4 + packages/common/types/checkout.ts | 2 + packages/common/types/playlist.ts | 13 ++ packages/common/types/settings.ts | 1 + .../hooks-react/src/useContentProtection.ts | 7 +- packages/hooks-react/src/useMediaSources.ts | 4 +- packages/hooks-react/src/useProtectedMedia.ts | 21 ++- .../containers/AccountModal/forms/Login.tsx | 1 - .../src/containers/Cinema/Cinema.test.tsx | 2 + platforms/web/ini/.webapp.dev.ini | 2 + 24 files changed, 460 insertions(+), 13 deletions(-) create mode 100644 packages/common/src/controllers/AccessController.ts create mode 100644 packages/common/src/modules/functions/getApiAccessBridgeUrl.ts create mode 100644 packages/common/src/services/AccessService.ts create mode 100644 packages/common/src/stores/AccessStore.ts create mode 100644 packages/common/types/access.ts diff --git a/packages/common/src/controllers/AccessController.ts b/packages/common/src/controllers/AccessController.ts new file mode 100644 index 000000000..e10d1d52a --- /dev/null +++ b/packages/common/src/controllers/AccessController.ts @@ -0,0 +1,178 @@ +import { inject, injectable } from 'inversify'; + +import type { IntegrationType } from '../../types/config'; +import type { AccessTokens } from '../../types/access'; +import ApiService from '../services/ApiService'; +import AccessService from '../services/AccessService'; +import AccountService from '../services/integrations/AccountService'; +import StorageService from '../services/StorageService'; +import { useConfigStore } from '../stores/ConfigStore'; +import { INTEGRATION_TYPE } from '../modules/types'; +import { getNamedModule } from '../modules/container'; +import { useAccountStore } from '../stores/AccountStore'; +import { ApiError } from '../utils/api'; +import { useAccessStore } from '../stores/AccessStore'; + +const ACCESS_TOKENS = 'access_tokens'; + +@injectable() +export default class AccessController { + private readonly apiService: ApiService; + private readonly accessService: AccessService; + private readonly accountService: AccountService; + private readonly storageService: StorageService; + + private siteId: string = ''; + + constructor( + @inject(INTEGRATION_TYPE) integrationType: IntegrationType, + @inject(ApiService) apiService: ApiService, + @inject(StorageService) storageService: StorageService, + @inject(AccessService) accessService: AccessService, + ) { + this.apiService = apiService; + this.accessService = accessService; + this.storageService = storageService; + this.accountService = getNamedModule(AccountService, integrationType); + } + + initialize = async () => { + const { config, accessModel } = useConfigStore.getState(); + this.siteId = config.siteId; + + // For the AVOD access model, signing and DRM are not supported, so access tokens generation is skipped + if (accessModel === 'AVOD') { + return; + } + + // Not awaiting to avoid blocking the loading process, + // as the initial access tokens can be stored asynchronously without affecting the app's performance + void this.generateOrRefreshAccessTokens(); + }; + + /** + * Retrieves media by its ID using a passport token. + * If no access tokens exist, it attempts to generate them, if the passport token is expired, it attempts to refresh them. + * If an access token retrieval fails or the user is not entitled to the content, an error is thrown. + */ + getMediaById = async (mediaId: string) => { + const { entitledPlan } = useAccountStore.getState(); + + if (!this.siteId || !entitledPlan) { + return; + } + + try { + const accessTokens = await this.generateOrRefreshAccessTokens(); + if (!accessTokens?.passport) { + throw new Error('Failed to get / generate access tokens and retrieve media.'); + } + return await this.apiService.getMediaByIdWithPassport({ id: mediaId, siteId: this.siteId, planId: entitledPlan.id, passport: accessTokens.passport }); + } catch (error: unknown) { + if (error instanceof ApiError && error.code === 403) { + // If the passport is invalid or expired, refresh the access tokens and try to get the media again. + const accessTokens = await this.refreshAccessTokens(); + if (accessTokens?.passport) { + return await this.apiService.getMediaByIdWithPassport({ id: mediaId, siteId: this.siteId, planId: entitledPlan.id, passport: accessTokens.passport }); + } + + throw new Error('Failed to refresh access tokens and retrieve media.'); + } + throw error; + } + }; + + /** + * Generates or refreshes access tokens based on their current validity. + * If existing tokens are expired, they are refreshed; if no tokens exist, they are generated. + * If the existing tokens are valid, it retrieves them. + */ + generateOrRefreshAccessTokens = async (): Promise => { + const existingAccessTokens = await this.getAccessTokens(); + const shouldRefresh = existingAccessTokens && Date.now() > existingAccessTokens.expires; + + if (!existingAccessTokens) { + await this.generateAccessTokens(); + } + + if (shouldRefresh) { + return await this.refreshAccessTokens(); + } + + return existingAccessTokens; + }; + + /** + * Generates access tokens based on the viewer auth data. + * If the viewer is not authenticated it generates only access for free plans (if they are defined). + * Stores the access tokens in local storage. + */ + generateAccessTokens = async (): Promise => { + if (!this.siteId) { + return null; + } + + const auth = await this.accountService.getAuthData(); + + const accessTokens = await this.accessService.generateAccessTokens(this.siteId, auth?.jwt); + if (accessTokens) { + await this.setAccessTokens(accessTokens); + return accessTokens; + } + + return null; + }; + + /** + * Refreshes the access tokens using the refresh token if they exist. + * If no tokens are found, it cannot refresh and returns null. + * Updates the localstorage with the newly generated access tokens. + */ + refreshAccessTokens = async (): Promise => { + const existingAccessTokens = await this.getAccessTokens(); + // there is no access tokens stored, nothing to refresh + if (!existingAccessTokens) { + return null; + } + + const accessTokens = await this.accessService.refreshAccessTokens(this.siteId, existingAccessTokens.refresh_token); + if (accessTokens) { + await this.setAccessTokens(accessTokens); + return accessTokens; + } + + return null; + }; + + /** + * Stores the access tokens in local storage, adding an expiration timestamp of 1 hour (passport validity). + * The expiration timestamp helps determine when the passport token should be refreshed. + */ + setAccessTokens = async (accessTokens: AccessTokens) => { + useAccessStore.setState({ passport: accessTokens.passport }); + // Since the actual valid time for a passport token is 1 hour, set the expires to one hour from now. + // The expires field here is used as a helper to manage the passport's validity and refresh process. + const expires = new Date(Date.now() + 3600 * 1000).getTime(); + return await this.storageService.setItem(ACCESS_TOKENS, JSON.stringify({ ...accessTokens, expires }), true); + }; + + /** + * Retrieves the access tokens from local storage (if any) along with their expiration timestamp. + */ + getAccessTokens = async (): Promise<(AccessTokens & { expires: number }) | null> => { + const accessTokens = await this.storageService.getItem(ACCESS_TOKENS, true, true); + if (accessTokens) { + useAccessStore.setState({ passport: accessTokens.passport }); + } + + return accessTokens; + }; + + /** + * Removes the access tokens from local storage (if any). + */ + removeAccessTokens = async () => { + useAccessStore.setState({ passport: null }); + return await this.storageService.removeItem(ACCESS_TOKENS); + }; +} diff --git a/packages/common/src/controllers/AccountController.ts b/packages/common/src/controllers/AccountController.ts index 3e6ec3c30..bbb9e4953 100644 --- a/packages/common/src/controllers/AccountController.ts +++ b/packages/common/src/controllers/AccountController.ts @@ -6,7 +6,9 @@ import type { IntegrationType } from '../../types/config'; import CheckoutService from '../services/integrations/CheckoutService'; import AccountService, { type AccountServiceFeatures } from '../services/integrations/AccountService'; import SubscriptionService from '../services/integrations/SubscriptionService'; +import JWPEntitlementService from '../services/JWPEntitlementService'; import type { Offer } from '../../types/checkout'; +import type { Plan } from '../../types/plans'; import type { Capture, Customer, @@ -16,7 +18,7 @@ import type { GetCaptureStatusResponse, SubscribeToNotificationsPayload, } from '../../types/account'; -import { assertFeature, assertModuleMethod, getNamedModule } from '../modules/container'; +import { assertFeature, assertModuleMethod, getModule, getNamedModule } from '../modules/container'; import { INTEGRATION_TYPE } from '../modules/types'; import type { ServiceResponse } from '../../types/service'; import { useAccountStore } from '../stores/AccountStore'; @@ -26,12 +28,15 @@ import { logError } from '../logger'; import WatchHistoryController from './WatchHistoryController'; import FavoritesController from './FavoritesController'; +import AccessController from './AccessController'; @injectable() export default class AccountController { private readonly checkoutService: CheckoutService; private readonly accountService: AccountService; private readonly subscriptionService: SubscriptionService; + private readonly entitlementService: JWPEntitlementService; + private readonly accessController: AccessController; private readonly favoritesController: FavoritesController; private readonly watchHistoryController: WatchHistoryController; private readonly features: AccountServiceFeatures; @@ -41,14 +46,17 @@ export default class AccountController { constructor( @inject(INTEGRATION_TYPE) integrationType: IntegrationType, + accessController: AccessController, favoritesController: FavoritesController, watchHistoryController: WatchHistoryController, ) { this.checkoutService = getNamedModule(CheckoutService, integrationType); this.accountService = getNamedModule(AccountService, integrationType); this.subscriptionService = getNamedModule(SubscriptionService, integrationType); + this.entitlementService = getModule(JWPEntitlementService); // @TODO: Controllers shouldn't be depending on other controllers, but we've agreed to keep this as is for now + this.accessController = accessController; this.favoritesController = favoritesController; this.watchHistoryController = watchHistoryController; @@ -85,6 +93,7 @@ export default class AccountController { useConfigStore.setState({ accessModel: this.accountService.accessModel }); await this.loadUserData(); + await this.getEntitledPlans(); useAccountStore.setState({ loading: false }); }; @@ -163,6 +172,7 @@ export default class AccountController { const response = await this.accountService.login({ email, password, referrer }); if (response) { + await this.accessController?.generateAccessTokens(); await this.afterLogin(response.user, response.customerConsents); return; } @@ -180,6 +190,7 @@ export default class AccountController { logout = async () => { await this.accountService?.logout(); + await this.accessController?.removeAccessTokens(); await this.clearLoginState(); // let the application know to refresh all entitlements @@ -380,6 +391,29 @@ export default class AccountController { return !!responseData?.accessGranted; }; + // This currently supports only one plan, as the current usage for the media metadata requires only one plan_id provided. + // TODO: Support for multiple plans should be added. Revisit this logic once the dependency on plan_id is changed. + getEntitledPlans = async (): Promise => { + const { config, settings } = useConfigStore.getState(); + const siteId = config.siteId; + const isAccessBridgeEnabled = !!settings?.apiAccessBridgeUrl; + + // This should be only used when access bridge is defined, regardless of the integration type. + if (!isAccessBridgeEnabled) { + return null; + } + + const response = await this.entitlementService.getEntitledPlans({ siteId }); + if (response?.plans?.length) { + // Find the SVOD plan or fallback to the first available plan + const entitledPlan = response.plans.find((plan) => plan.metadata.access_model === 'svod') || response.plans[0]; + useAccountStore.setState({ entitledPlan }); + return entitledPlan; + } + + return null; + }; + reloadSubscriptions = async ( { delay, retry }: { delay?: number; retry?: number } = { delay: 0, diff --git a/packages/common/src/controllers/AppController.ts b/packages/common/src/controllers/AppController.ts index 474d04c86..803f5c867 100644 --- a/packages/common/src/controllers/AppController.ts +++ b/packages/common/src/controllers/AppController.ts @@ -15,6 +15,7 @@ import { logDebug } from '../logger'; import WatchHistoryController from './WatchHistoryController'; import FavoritesController from './FavoritesController'; import AccountController from './AccountController'; +import AccessController from './AccessController'; @injectable() export default class AppController { @@ -82,6 +83,11 @@ export default class AppController { await getModule(AccountController).initialize(url, refreshEntitlements); } + // when the apiAccessBridgeUrl is set up in the .ini file, we initialize the AccessController + if (settings?.apiAccessBridgeUrl) { + await getModule(AccessController).initialize(); + } + if (config.features?.continueWatchingList && config.content.some((el) => el.type === PersonalShelf.ContinueWatching)) { await getModule(WatchHistoryController).initialize(language); } @@ -116,4 +122,11 @@ export default class AppController { return configState.integrationType; }; + + getApiAccessBridgeUrl = (): string | undefined => { + const configState = useConfigStore.getState(); + + if (!configState.loaded) throw new Error('A call to `AppController#getApiAccessBridgeUrl()` was made before loading the config'); + return configState.settings?.apiAccessBridgeUrl || undefined; + }; } diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index 50c854c56..a791fea8e 100644 --- a/packages/common/src/env.ts +++ b/packages/common/src/env.ts @@ -1,6 +1,7 @@ export type Env = { APP_VERSION: string; APP_API_BASE_URL: string; + APP_API_ACCESS_BRIDGE_URL: string; APP_PLAYER_ID: string; APP_FOOTER_TEXT: string; APP_DEFAULT_LANGUAGE: string; @@ -15,6 +16,7 @@ export type Env = { const env: Env = { APP_VERSION: '', APP_API_BASE_URL: 'https://cdn.jwplayer.com', + APP_API_ACCESS_BRIDGE_URL: '', APP_PLAYER_ID: 'M4qoGvUk', APP_FOOTER_TEXT: '', APP_DEFAULT_LANGUAGE: 'en', @@ -23,6 +25,7 @@ const env: Env = { export const configureEnv = (options: Partial) => { env.APP_VERSION = options.APP_VERSION || env.APP_VERSION; env.APP_API_BASE_URL = options.APP_API_BASE_URL || env.APP_API_BASE_URL; + env.APP_API_ACCESS_BRIDGE_URL = options.APP_API_ACCESS_BRIDGE_URL || env.APP_API_ACCESS_BRIDGE_URL; env.APP_PLAYER_ID = options.APP_PLAYER_ID || env.APP_PLAYER_ID; env.APP_FOOTER_TEXT = options.APP_FOOTER_TEXT || env.APP_FOOTER_TEXT; env.APP_DEFAULT_LANGUAGE = options.APP_DEFAULT_LANGUAGE || env.APP_DEFAULT_LANGUAGE; diff --git a/packages/common/src/modules/functions/getApiAccessBridgeUrl.ts b/packages/common/src/modules/functions/getApiAccessBridgeUrl.ts new file mode 100644 index 000000000..ac33a02ff --- /dev/null +++ b/packages/common/src/modules/functions/getApiAccessBridgeUrl.ts @@ -0,0 +1,12 @@ +import type { interfaces } from 'inversify'; + +import AppController from '../../controllers/AppController'; + +/** + * Retrieves the access bridge URL from the AppController. + * If the access bridge URL is defined in the application's .ini configuration file, + * the function returns the URL. If the value is not defined, it returns `undefined`. + */ +export const getApiAccessBridgeUrl = (context: interfaces.Context) => { + return context.container.get(AppController).getApiAccessBridgeUrl(); +}; diff --git a/packages/common/src/modules/register.ts b/packages/common/src/modules/register.ts index 131d93bf8..8bad8ece3 100644 --- a/packages/common/src/modules/register.ts +++ b/packages/common/src/modules/register.ts @@ -3,7 +3,7 @@ import 'reflect-metadata'; // include once in the app for inversify (see: https://github.com/inversify/InversifyJS/blob/master/README.md#-installation) import { INTEGRATION, EPG_TYPE } from '../constants'; import { container } from './container'; -import { DETERMINE_INTEGRATION_TYPE, INTEGRATION_TYPE } from './types'; +import { API_ACCESS_BRIDGE_URL, DETERMINE_INTEGRATION_TYPE, INTEGRATION_TYPE } from './types'; import ApiService from '../services/ApiService'; import WatchHistoryService from '../services/WatchHistoryService'; @@ -16,6 +16,7 @@ import SettingsService from '../services/SettingsService'; import WatchHistoryController from '../controllers/WatchHistoryController'; import CheckoutController from '../controllers/CheckoutController'; import AccountController from '../controllers/AccountController'; +import AccessController from '../controllers/AccessController'; import FavoritesController from '../controllers/FavoritesController'; import AppController from '../controllers/AppController'; import EpgController from '../controllers/EpgController'; @@ -25,6 +26,10 @@ import EpgService from '../services/EpgService'; import ViewNexaEpgService from '../services/epg/ViewNexaEpgService'; import JWEpgService from '../services/epg/JWEpgService'; +// Access integration +import AccessService from '../services/AccessService'; +import { getApiAccessBridgeUrl } from './functions/getApiAccessBridgeUrl'; + // Integration interfaces import AccountService from '../services/integrations/AccountService'; import CheckoutService from '../services/integrations/CheckoutService'; @@ -51,6 +56,7 @@ container.bind(FavoriteService).toSelf(); container.bind(GenericEntitlementService).toSelf(); container.bind(ApiService).toSelf(); container.bind(SettingsService).toSelf(); +container.bind(AccessService).toSelf(); // Common controllers container.bind(AppController).toSelf(); @@ -61,6 +67,7 @@ container.bind(EpgController).toSelf(); // Integration controllers container.bind(AccountController).toSelf(); container.bind(CheckoutController).toSelf(); +container.bind(AccessController).toSelf(); // EPG services container.bind(EpgService).to(JWEpgService).whenTargetNamed(EPG_TYPE.jwp); @@ -68,6 +75,7 @@ container.bind(EpgService).to(ViewNexaEpgService).whenTargetNamed(EPG_TYPE.viewN // Functions container.bind(INTEGRATION_TYPE).toDynamicValue(getIntegrationType); +container.bind(API_ACCESS_BRIDGE_URL).toDynamicValue(getApiAccessBridgeUrl); // Cleeng integration container.bind(DETERMINE_INTEGRATION_TYPE).toConstantValue(isCleengIntegrationType); diff --git a/packages/common/src/modules/types.ts b/packages/common/src/modules/types.ts index c00c26851..0fb528923 100644 --- a/packages/common/src/modules/types.ts +++ b/packages/common/src/modules/types.ts @@ -3,3 +3,5 @@ export const INTEGRATION_TYPE = Symbol('INTEGRATION_TYPE'); export const DETERMINE_INTEGRATION_TYPE = Symbol('DETERMINE_INTEGRATION_TYPE'); export const GET_CUSTOMER_IP = Symbol('GET_CUSTOMER_IP'); + +export const API_ACCESS_BRIDGE_URL = Symbol('API_ACCESS_BRIDGE_URL'); diff --git a/packages/common/src/services/AccessService.ts b/packages/common/src/services/AccessService.ts new file mode 100644 index 000000000..4667bde58 --- /dev/null +++ b/packages/common/src/services/AccessService.ts @@ -0,0 +1,59 @@ +import { inject, injectable } from 'inversify'; + +import type { AccessTokens } from '../../types/access'; +import { logError } from '../logger'; +import { API_ACCESS_BRIDGE_URL } from '../modules/types'; + +@injectable() +export default class AccessService { + private readonly apiAccessBridgeUrl; + + constructor(@inject(API_ACCESS_BRIDGE_URL) apiAccessBridgeUrl: string) { + this.apiAccessBridgeUrl = apiAccessBridgeUrl; + } + + generateAccessTokens = async (siteId: string, jwt?: string): Promise => { + const url = `${this.apiAccessBridgeUrl}/v2/sites/${siteId}/access/generate`; + const response = await fetch(url, { + method: 'PUT', + headers: { + Authorization: jwt ? `Bearer ${jwt}` : '', + }, + }); + + if (!response.ok) { + logError('AccessService', 'Failed to generateAccessTokens', { + status: response.status, + error: response.json(), + }); + + return null; + } + + return (await response.json()) as AccessTokens; + }; + + refreshAccessTokens = async (siteId: string, refresh_token: string): Promise => { + const url = `${this.apiAccessBridgeUrl}/v2/sites/${siteId}/access/refresh`; + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + refresh_token, + }), + }); + + if (!response.ok) { + logError('AccessService', 'Failed to refreshAccessTokens', { + status: response.status, + error: response.json(), + }); + + return null; + } + + return (await response.json()) as AccessTokens; + }; +} diff --git a/packages/common/src/services/ApiService.ts b/packages/common/src/services/ApiService.ts index 0089768e2..0f36588c1 100644 --- a/packages/common/src/services/ApiService.ts +++ b/packages/common/src/services/ApiService.ts @@ -138,7 +138,7 @@ export default class ApiService { return transformedMediaItem; }; - private transformEpisodes = (episodesRes: EpisodesRes, language: string, seasonNumber?: number) => { + private transformEpisodes = (episodesRes: EpisodesRes, language?: string, seasonNumber?: number) => { const { episodes, page, page_limit, total } = episodesRes; // Adding images and keys for media items @@ -207,6 +207,33 @@ export default class ApiService { return this.transformMediaItem({ item: mediaItem, language }); }; + /** + * Get media by id with passport + */ + getMediaByIdWithPassport = async ({ + id, + siteId, + planId, + passport, + language, + }: { + id: string; + siteId: string; + planId: string; + passport: string; + language?: string; + }): Promise => { + const pathname = `/v2/sites/${siteId}/media/${id}/playback.json`; + const url = createURL(`${env.APP_API_BASE_URL}${pathname}`, { passport, plan_id: planId }); + const response = await fetch(url); + const data = (await getDataOrThrow(response)) as Playlist; + const mediaItem = data.playlist[0]; + + if (!mediaItem) throw new Error('MediaItem not found'); + + return this.transformMediaItem({ item: mediaItem, language }); + }; + /** * Get series by id * @param {string} id @@ -249,7 +276,7 @@ export default class ApiService { pageOffset?: number; pageLimit?: number; afterId?: string; - language: string; + language?: string; }): Promise => { if (!seriesId) { throw new Error('Series ID is required'); diff --git a/packages/common/src/services/JWPEntitlementService.ts b/packages/common/src/services/JWPEntitlementService.ts index 75a9d1ec4..04003abbe 100644 --- a/packages/common/src/services/JWPEntitlementService.ts +++ b/packages/common/src/services/JWPEntitlementService.ts @@ -1,5 +1,8 @@ import { inject, injectable } from 'inversify'; +import type { GetEntitledPlans } from '../../types/checkout'; +import type { PlansResponse } from '../../types/plans'; + import type { SignedMediaResponse } from './integrations/jwp/types'; import JWPAPIService from './integrations/jwp/JWPAPIService'; @@ -29,4 +32,15 @@ export default class JWPEntitlementService { throw new Error('Unauthorized'); } }; + + getEntitledPlans: GetEntitledPlans = async ({ siteId }) => { + try { + const data = await this.apiService.get(`/v3/sites/${siteId}/entitlements`, { + withAuthentication: await this.apiService.isAuthenticated(), + }); + return data; + } catch { + throw new Error('Failed to fetch entitled plans'); + } + }; } diff --git a/packages/common/src/services/SettingsService.ts b/packages/common/src/services/SettingsService.ts index 2b964082b..b0ab6b8be 100644 --- a/packages/common/src/services/SettingsService.ts +++ b/packages/common/src/services/SettingsService.ts @@ -103,6 +103,7 @@ export default class SettingsService { settings.defaultConfigSource ||= env.APP_DEFAULT_CONFIG_SOURCE; settings.playerId ||= env.APP_PLAYER_ID || OTT_GLOBAL_PLAYER_ID; settings.playerLicenseKey ||= env.APP_PLAYER_LICENSE_KEY; + settings.apiAccessBridgeUrl ||= env.APP_API_ACCESS_BRIDGE_URL; // The player key should be set if using the global ott player if (settings.playerId === OTT_GLOBAL_PLAYER_ID && !settings.playerLicenseKey) { diff --git a/packages/common/src/stores/AccessStore.ts b/packages/common/src/stores/AccessStore.ts new file mode 100644 index 000000000..e98579e17 --- /dev/null +++ b/packages/common/src/stores/AccessStore.ts @@ -0,0 +1,9 @@ +import { createStore } from './utils'; + +type AccessStore = { + passport: string | null; +}; + +export const useAccessStore = createStore('AccessStore', () => ({ + passport: null, +})); diff --git a/packages/common/src/stores/AccountStore.ts b/packages/common/src/stores/AccountStore.ts index ad7aa3a39..5e830632a 100644 --- a/packages/common/src/stores/AccountStore.ts +++ b/packages/common/src/stores/AccountStore.ts @@ -1,3 +1,4 @@ +import type { Plan } from '../../types/plans'; import type { CustomFormField, Customer, CustomerConsent } from '../../types/account'; import type { Offer } from '../../types/checkout'; import type { PaymentDetail, Subscription, Transaction } from '../../types/subscription'; @@ -9,6 +10,7 @@ type AccountStore = { user: Customer | null; subscription: Subscription | null; transactions: Transaction[] | null; + entitledPlan: Plan | null; activePayment: PaymentDetail | null; customerConsents: CustomerConsent[] | null; publisherConsents: CustomFormField[] | null; @@ -23,6 +25,7 @@ export const useAccountStore = createStore('AccountStore', (set, g user: null, subscription: null, transactions: null, + entitledPlan: null, activePayment: null, customerConsents: null, publisherConsents: null, diff --git a/packages/common/src/utils/sources.ts b/packages/common/src/utils/sources.ts index c96a35022..0b666b47d 100644 --- a/packages/common/src/utils/sources.ts +++ b/packages/common/src/utils/sources.ts @@ -10,7 +10,19 @@ const isBCLManifestType = (sourceUrl: string, baseUrl: string, mediaId: string, return extensions.some((ext) => sourceUrl === `${baseUrl}/live/broadcast/${mediaId}.${ext}`); }; -export const getSources = ({ item, baseUrl, config, user }: { item: PlaylistItem; baseUrl: string; config: Config; user: Customer | null }) => { +export const getSources = ({ + item, + baseUrl, + config, + user, + passport, +}: { + item: PlaylistItem; + baseUrl: string; + config: Config; + user: Customer | null; + passport: string | null; +}) => { const { sources, mediaid } = item; const { adConfig, siteId, adDeliveryMethod } = config; @@ -36,8 +48,37 @@ export const getSources = ({ item, baseUrl, config, user }: { item: PlaylistItem url.searchParams.set('user_id', userId); } - source.file = url.toString(); + // Attach the passport in all the drm sources as it's needed for the licence request. + // Passport is only available if Access Bridge is in use. + if (passport) { + attachPassportToSourceWithDRM(source, passport); + } + source.file = url.toString(); return source; }); }; + +function attachPassportToSourceWithDRM(source: Source, passport: string): Source { + function updateUrl(urlString: string, passport: string): string { + const url = new URL(urlString); + if (!url.searchParams.has('token')) { + url.searchParams.set('passport', passport); + } + return url.toString(); + } + + if (source?.drm) { + if (source.drm?.playready?.url) { + source.drm.playready.url = updateUrl(source.drm.playready.url, passport); + } + if (source.drm?.widevine?.url) { + source.drm.widevine.url = updateUrl(source.drm.widevine.url, passport); + } + if (source.drm?.fairplay?.processSpcUrl) { + source.drm.fairplay.processSpcUrl = updateUrl(source.drm.fairplay.processSpcUrl, passport); + } + } + + return source; +} diff --git a/packages/common/types/access.ts b/packages/common/types/access.ts new file mode 100644 index 000000000..2a081dfbf --- /dev/null +++ b/packages/common/types/access.ts @@ -0,0 +1,4 @@ +export type AccessTokens = { + passport: string; + refresh_token: string; +}; diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index 89210f117..584dfc1eb 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -1,6 +1,7 @@ import type { PayloadWithIPOverride } from './account'; import type { PaymentDetail } from './subscription'; import type { EmptyEnvironmentServiceRequest, EnvironmentServiceRequest, PromiseRequest } from './service'; +import type { PlansResponse } from './plans'; export type Offer = { id: number | null; @@ -384,3 +385,4 @@ export type DeletePaymentMethod = EnvironmentServiceRequest; export type FinalizeAdyenPaymentDetails = EnvironmentServiceRequest; export type GetDirectPostCardPayment = (cardPaymentPayload: CardPaymentData, order: Order, referrer: string, returnUrl: string) => Promise; +export type GetEntitledPlans = PromiseRequest<{ siteId: string }, PlansResponse>; diff --git a/packages/common/types/playlist.ts b/packages/common/types/playlist.ts index 26c5731c3..5d08d9ca2 100644 --- a/packages/common/types/playlist.ts +++ b/packages/common/types/playlist.ts @@ -8,9 +8,22 @@ export type Image = { width: number; }; +export type DRM = { + playready?: { + url: string; + }; + widevine?: { + url: string; + }; + fairplay?: { + processSpcUrl: string; + }; +}; + export type Source = { file: string; type: string; + drm?: DRM; }; export type Track = { diff --git a/packages/common/types/settings.ts b/packages/common/types/settings.ts index 38eaab547..b030894c0 100644 --- a/packages/common/types/settings.ts +++ b/packages/common/types/settings.ts @@ -4,4 +4,5 @@ export type Settings = { playerLicenseKey?: string; additionalAllowedConfigSources?: string[]; UNSAFE_allowAnyConfigSource?: boolean; + apiAccessBridgeUrl?: string; }; diff --git a/packages/hooks-react/src/useContentProtection.ts b/packages/hooks-react/src/useContentProtection.ts index a3598af93..56580b69b 100644 --- a/packages/hooks-react/src/useContentProtection.ts +++ b/packages/hooks-react/src/useContentProtection.ts @@ -20,13 +20,15 @@ const useContentProtection = ( const genericEntitlementService = getModule(GenericEntitlementService); const jwpEntitlementService = getModule(JWPEntitlementService); - const { configId, signingConfig, contentProtection, jwp, urlSigning } = useConfigStore(({ config }) => ({ + const { configId, signingConfig, contentProtection, jwp, urlSigning, isAccessBridgeEnabled } = useConfigStore(({ config, settings }) => ({ configId: config.id, signingConfig: config.contentSigningService, contentProtection: config.contentProtection, jwp: config.integrations.jwp, urlSigning: isTruthyCustomParamValue(config?.custom?.urlSigning), + isAccessBridgeEnabled: !!settings?.apiAccessBridgeUrl, })); + const host = signingConfig?.host; const drmPolicyId = contentProtection?.drm?.defaultPolicyId ?? signingConfig?.drmPolicyId; const signingEnabled = !!urlSigning || !!host || (!!drmPolicyId && !host); @@ -42,12 +44,13 @@ const useContentProtection = ( return genericEntitlementService.getMediaToken(host, id, authData?.jwt, params, drmPolicyId); } + // if provider is JWP if (jwp && configId && !!id && signingEnabled) { return jwpEntitlementService.getJWPMediaToken(configId, id); } }, - { enabled: signingEnabled && enabled && !!id, keepPreviousData: false, staleTime: 15 * 60 * 1000 }, + { enabled: signingEnabled && enabled && !!id && !isAccessBridgeEnabled, keepPreviousData: false, staleTime: 15 * 60 * 1000 }, ); const queryResult = useQuery([type, id, params, token], async () => callback(token, drmPolicyId), { diff --git a/packages/hooks-react/src/useMediaSources.ts b/packages/hooks-react/src/useMediaSources.ts index 8f344f018..beca034f0 100644 --- a/packages/hooks-react/src/useMediaSources.ts +++ b/packages/hooks-react/src/useMediaSources.ts @@ -3,11 +3,13 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; import type { PlaylistItem, Source } from '@jwp/ott-common/types/playlist'; import { getSources } from '@jwp/ott-common/src/utils/sources'; +import { useAccessStore } from '@jwp/ott-common/src/stores/AccessStore'; /** Modify manifest URLs to handle server ads and analytics params */ export const useMediaSources = ({ item, baseUrl }: { item: PlaylistItem; baseUrl: string }): Source[] => { const config = useConfigStore((s) => s.config); const user = useAccountStore((s) => s.user); + const passport = useAccessStore((s) => s.passport); - return useMemo(() => getSources({ item, baseUrl, config, user }), [item, baseUrl, config, user]); + return useMemo(() => getSources({ item, baseUrl, config, user, passport }), [item, baseUrl, config, user, passport]); }; diff --git a/packages/hooks-react/src/useProtectedMedia.ts b/packages/hooks-react/src/useProtectedMedia.ts index f034e3378..dda954fee 100644 --- a/packages/hooks-react/src/useProtectedMedia.ts +++ b/packages/hooks-react/src/useProtectedMedia.ts @@ -2,14 +2,29 @@ import { useQuery } from 'react-query'; import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; +import AccessController from '@jwp/ott-common/src/controllers/AccessController'; +import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import useContentProtection from './useContentProtection'; export default function useProtectedMedia(item: PlaylistItem) { const apiService = getModule(ApiService); - const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => - apiService.getMediaById({ id: item.mediaid, token, drmPolicyId }), - ); + const accessController = getModule(AccessController); + + const { isAccessBridgeEnabled } = useConfigStore(({ settings }) => ({ + isAccessBridgeEnabled: !!settings?.apiAccessBridgeUrl, + })); + + const contentProtectionQuery = useContentProtection('media', item.mediaid, async (token, drmPolicyId) => { + // If the Access Bridge is enabled, use it to retrieve media via access passport. + // This bypasses the need for a DRM token or policy and directly uses the access-controlled method. + if (isAccessBridgeEnabled) { + return accessController.getMediaById(item.mediaid); + } + + // If Access Bridge is not enabled, retrieve the media using the provided DRM token and policy ID. + return apiService.getMediaById({ id: item.mediaid, token, drmPolicyId }); + }); const { isLoading, data: isGeoBlocked } = useQuery( ['media', 'geo', item.mediaid], diff --git a/packages/ui-react/src/containers/AccountModal/forms/Login.tsx b/packages/ui-react/src/containers/AccountModal/forms/Login.tsx index 7d016a11b..010866901 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/Login.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/Login.tsx @@ -35,7 +35,6 @@ const Login = () => { onSubmit: ({ email, password }) => accountController.login(email, password, window.location.href), onSubmitSuccess: () => { announce(t('login.sign_in_success'), 'success'); - navigate(modalURLFromLocation(location, null)); }, onSubmitError: ({ resetValue }) => resetValue('password'), diff --git a/packages/ui-react/src/containers/Cinema/Cinema.test.tsx b/packages/ui-react/src/containers/Cinema/Cinema.test.tsx index 1711f49e4..d93b72fea 100644 --- a/packages/ui-react/src/containers/Cinema/Cinema.test.tsx +++ b/packages/ui-react/src/containers/Cinema/Cinema.test.tsx @@ -3,6 +3,7 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import { beforeEach } from 'vitest'; import { mockService } from '@jwp/ott-common/test/mockService'; import ApiService from '@jwp/ott-common/src/services/ApiService'; +import AccessController from '@jwp/ott-common/src/controllers/AccessController'; import GenericEntitlementService from '@jwp/ott-common/src/services/GenericEntitlementService'; import JWPEntitlementService from '@jwp/ott-common/src/services/JWPEntitlementService'; import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; @@ -18,6 +19,7 @@ describe('', () => { mockService(GenericEntitlementService, {}); mockService(JWPEntitlementService, {}); mockService(WatchHistoryController, {}); + mockService(AccessController, {}); }); test('renders and matches snapshot', async () => { diff --git a/platforms/web/ini/.webapp.dev.ini b/platforms/web/ini/.webapp.dev.ini index 01fd5196a..850dc82fd 100644 --- a/platforms/web/ini/.webapp.dev.ini +++ b/platforms/web/ini/.webapp.dev.ini @@ -2,3 +2,5 @@ defaultConfigSource = gnnuzabk ; When developing, switching between configs is useful for test and debug UNSAFE_allowAnyConfigSource = true +; Access Bridge service API url host +apiAccessBridgeUrl = From 4ffb84923424de05f1f62455c7822354c0842173 Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:45:50 +0200 Subject: [PATCH 10/16] feat(menu): fix support for media menu item (#621) --- packages/common/src/utils/configSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/utils/configSchema.ts b/packages/common/src/utils/configSchema.ts index 43c2d60af..37c3dd62d 100644 --- a/packages/common/src/utils/configSchema.ts +++ b/packages/common/src/utils/configSchema.ts @@ -14,7 +14,7 @@ const menuSchema: SchemaOf = object().shape({ label: string().defined(), contentId: string().defined(), filterTags: string().notRequired(), - type: mixed().oneOf(['playlist', 'content_list']).notRequired(), + type: mixed().oneOf(['playlist', 'content_list', 'media']).notRequired(), }); const featuresSchema: SchemaOf = object({ From 3dea0fc709cc5bf4bb2bea0c011f37cf8d9f227b Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:55:26 +0200 Subject: [PATCH 11/16] [OWA-89] feat: rewrite mediaURL function (#620) * feat: rewrite mediaURL function * feat: use menu label as title * feat: use label for content list and playlist formatters as well * feat: destructure props * feat: code cleanup * feat: add small code improvement --- packages/common/src/utils/structuredData.ts | 3 ++- .../common/src/utils/urlFormatting.test.ts | 4 +++- packages/common/src/utils/urlFormatting.ts | 20 +++++++++---------- .../src/components/Favorites/Favorites.tsx | 2 +- .../ui-react/src/components/Shelf/Shelf.tsx | 3 ++- .../ui-react/src/containers/Layout/Layout.tsx | 2 +- .../MediaEpisode/MediaEpisode.tsx | 15 +++++++++++++- .../mediaScreens/MediaEvent/MediaEvent.tsx | 13 ++++++------ .../mediaScreens/MediaMovie/MediaMovie.tsx | 10 +++++----- .../mediaScreens/MediaSeries/MediaSeries.tsx | 2 +- .../MediaStaticPage/MediaStaticPage.tsx | 2 +- .../PlaylistGrid/PlaylistGrid.tsx | 2 +- packages/ui-react/src/pages/Search/Search.tsx | 3 ++- 13 files changed, 49 insertions(+), 32 deletions(-) diff --git a/packages/common/src/utils/structuredData.ts b/packages/common/src/utils/structuredData.ts index 6470ae526..292076a2b 100644 --- a/packages/common/src/utils/structuredData.ts +++ b/packages/common/src/utils/structuredData.ts @@ -5,7 +5,8 @@ import { mediaURL } from './urlFormatting'; import { secondsToISO8601 } from './datetime'; export const generateMovieJSONLD = (item: PlaylistItem, origin: string) => { - const movieCanonical = `${origin}${mediaURL({ media: item })}`; + const { mediaid: id, title } = item; + const movieCanonical = `${origin}${mediaURL({ id, title })}`; return JSON.stringify({ '@context': 'http://schema.org/', diff --git a/packages/common/src/utils/urlFormatting.test.ts b/packages/common/src/utils/urlFormatting.test.ts index 4fc93fc1f..c0c616f4a 100644 --- a/packages/common/src/utils/urlFormatting.test.ts +++ b/packages/common/src/utils/urlFormatting.test.ts @@ -35,7 +35,9 @@ describe('createPath, mediaURL, playlistURL and liveChannelsURL', () => { test('valid media path', () => { const playlist = playlistFixture as Playlist; const media = playlist.playlist[0] as PlaylistItem; - const url = mediaURL({ media, playlistId: playlist.feedid, play: true }); + + const { mediaid: id, title } = media; + const url = mediaURL({ id, title, playlistId: playlist.feedid, play: true }); expect(url).toEqual('/m/uB8aRnu6/agent-327?r=dGSUzs9o&play=1'); }); diff --git a/packages/common/src/utils/urlFormatting.ts b/packages/common/src/utils/urlFormatting.ts index cc968c82f..de2f8ada5 100644 --- a/packages/common/src/utils/urlFormatting.ts +++ b/packages/common/src/utils/urlFormatting.ts @@ -99,19 +99,21 @@ export const slugify = (text: string, whitespaceChar: string = '-') => .replace(/-/g, whitespaceChar); export const mediaURL = ({ - media, + id, + title, playlistId, play = false, episodeId, }: { - media: PlaylistItem; + id: string; + title?: string; playlistId?: string | null; play?: boolean; episodeId?: string; }) => { return createPath( PATH_MEDIA, - { id: media.mediaid, title: slugify(media.title) }, + { id, title: title ? slugify(title) : undefined }, { r: playlistId, play: play ? '1' : null, @@ -120,10 +122,6 @@ export const mediaURL = ({ ); }; -export const singleMediaURL = (id: string, title?: string) => { - return createPath(PATH_MEDIA, { id, title: title ? slugify(title) : undefined }); -}; - export const playlistURL = (id: string, title?: string) => { return createPath(PATH_PLAYLIST, { id, title: title ? slugify(title) : undefined }); }; @@ -132,14 +130,14 @@ export const contentListURL = (id: string, title?: string) => { return createPath(PATH_CONTENT_LIST, { id, title: title ? slugify(title) : undefined }); }; -export const determinePath = ({ type, contentId }: { type: AppMenuType | undefined; contentId: string }) => { +export const determinePath = ({ type, contentId, label }: { type: AppMenuType | undefined; contentId: string; label?: string }) => { switch (type) { case APP_CONFIG_ITEM_TYPE.content_list: - return contentListURL(contentId); + return contentListURL(contentId, label); case APP_CONFIG_ITEM_TYPE.media: - return singleMediaURL(contentId); + return mediaURL({ id: contentId, title: label }); case APP_CONFIG_ITEM_TYPE.playlist: - return playlistURL(contentId); + return playlistURL(contentId, label); default: return ''; } diff --git a/packages/ui-react/src/components/Favorites/Favorites.tsx b/packages/ui-react/src/components/Favorites/Favorites.tsx index 4d991ba59..237417b08 100644 --- a/packages/ui-react/src/components/Favorites/Favorites.tsx +++ b/packages/ui-react/src/components/Favorites/Favorites.tsx @@ -28,7 +28,7 @@ const cols: Breakpoints = { const Favorites = ({ playlist, accessModel, hasSubscription, onCardHover, onClearFavoritesClick }: Props): JSX.Element => { const { t } = useTranslation('user'); - const getURL = (playlistItem: PlaylistItem) => mediaURL({ media: playlistItem, playlistId: playlistItem.feedid }); + const getURL = (playlistItem: PlaylistItem) => mediaURL({ id: playlistItem.mediaid, title: playlistItem.title, playlistId: playlistItem.feedid }); return (
    diff --git a/packages/ui-react/src/components/Shelf/Shelf.tsx b/packages/ui-react/src/components/Shelf/Shelf.tsx index 92c9eb2d6..c24457903 100644 --- a/packages/ui-react/src/components/Shelf/Shelf.tsx +++ b/packages/ui-react/src/components/Shelf/Shelf.tsx @@ -76,7 +76,8 @@ const Shelf = ({ const renderTile = useCallback( ({ item, isVisible }: { item: PlaylistItem; isVisible: boolean }) => { - const url = mediaURL({ media: item, playlistId: playlist.feedid, play: type === PersonalShelf.ContinueWatching }); + const { mediaid: id, title } = item; + const url = mediaURL({ id, title, playlistId: playlist.feedid, play: type === PersonalShelf.ContinueWatching }); return ( { { label: t('home'), to: '/' }, ...menu.map(({ label, contentId, type }) => ({ label, - to: determinePath({ type, contentId }), + to: determinePath({ type, contentId, label }), })), ]; diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx index d0edbb727..b4c61c440 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEpisode/MediaEpisode.tsx @@ -30,8 +30,21 @@ const MediaEpisode: ScreenComponent = ({ data: media, isLoading: i return ; } + const { mediaid: id, title } = seriesMedia as PlaylistItem; + // Use media episode item for legacy series flow - return ; + return ( + + ); }; export default MediaEpisode; diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx index d6da6c8f2..8ecccd493 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaEvent/MediaEvent.tsx @@ -65,8 +65,8 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = const hasMediaOffers = !!mediaOffers.length; // Handlers - const goBack = () => media && navigate(mediaURL({ media, playlistId, play: false })); - const getUrl = (item: PlaylistItem) => mediaURL({ media: item, playlistId }); + const goBack = () => media && navigate(mediaURL({ id: media.mediaid, title: media.title, playlistId, play: false })); + const getUrl = (item: PlaylistItem) => mediaURL({ id: item.mediaid, title: item.title, playlistId }); const handleComplete = useCallback(() => { if (!id || !playlist) return; @@ -78,7 +78,7 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = return; } - return nextItem && navigate(mediaURL({ media: nextItem, playlistId, play: true })); + return nextItem && navigate(mediaURL({ id: nextItem.mediaid, title: nextItem.title, playlistId, play: true })); }, [id, playlist, navigate, playlistId]); // Effects @@ -88,8 +88,9 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = }, [id]); // UI - const pageTitle = `${media.title} - ${siteName}`; - const canonicalUrl = media ? `${window.location.origin}${mediaURL({ media: media })}` : window.location.href; + const { title, mediaid } = media; + const pageTitle = `${title} - ${siteName}`; + const canonicalUrl = media ? `${window.location.origin}${mediaURL({ id: mediaid, title })}` : window.location.href; const primaryMetadata = ( <> @@ -103,7 +104,7 @@ const MediaEvent: ScreenComponent = ({ data: media, isLoading }) = ); diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx index dd3d5df03..feccb3110 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaMovie/MediaMovie.tsx @@ -59,8 +59,8 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { const hasMediaOffers = !!mediaOffers.length; // Handlers - const goBack = () => data && navigate(mediaURL({ media: data, playlistId: feedId, play: false })); - const getUrl = (item: PlaylistItem) => mediaURL({ media: item, playlistId: features?.recommendationsPlaylist }); + const goBack = () => data && navigate(mediaURL({ id: data.mediaid, title: data.title, playlistId: feedId, play: false })); + const getUrl = (item: PlaylistItem) => mediaURL({ id: item.mediaid, title: item.title, playlistId: features?.recommendationsPlaylist }); const handleComplete = useCallback(() => { if (!id || !playlist) return; @@ -68,7 +68,7 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { const index = playlist.playlist.findIndex(({ mediaid }) => mediaid === id); const nextItem = playlist.playlist[index + 1]; - return nextItem && navigate(mediaURL({ media: nextItem, playlistId: features?.recommendationsPlaylist, play: true })); + return nextItem && navigate(mediaURL({ id: nextItem.mediaid, title: nextItem.title, playlistId: features?.recommendationsPlaylist, play: true })); }, [id, playlist, navigate, features?.recommendationsPlaylist]); useEffect(() => { @@ -78,7 +78,7 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { // UI const pageTitle = `${data.title} - ${siteName}`; - const canonicalUrl = data ? `${window.location.origin}${mediaURL({ media: data })}` : window.location.href; + const canonicalUrl = data ? `${window.location.origin}${mediaURL({ id: data.mediaid, title: data.title })}` : window.location.href; const primaryMetadata = ; const shareButton = ; @@ -86,7 +86,7 @@ const MediaMovie: ScreenComponent = ({ data, isLoading }) => { ); diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx index 37c617bc8..ea2cd831f 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaSeries/MediaSeries.tsx @@ -193,7 +193,7 @@ const MediaSeries: ScreenComponent = ({ data: seriesMedia }) => { if (!seriesMedia || !series || !playEpisode) return ; const pageTitle = `${selectedItem.title} - ${siteName}`; - const canonicalUrl = `${window.location.origin}${mediaURL({ media: seriesMedia, episodeId: episode?.mediaid })}`; + const canonicalUrl = `${window.location.origin}${mediaURL({ id: seriesMedia.mediaid, title: seriesMedia.title, episodeId: episode?.mediaid })}`; const primaryMetadata = ; const secondaryMetadata = episodeMetadata && episode && ( diff --git a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx index ae66dbded..508933b8b 100644 --- a/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/mediaScreens/MediaStaticPage/MediaStaticPage.tsx @@ -14,7 +14,7 @@ const MediaStaticPage: ScreenComponent = ({ data }) => { const { config } = useConfigStore(({ config }) => ({ config }), shallow); const { siteName } = config; const pageTitle = `${data.title} - ${siteName}`; - const canonicalUrl = data ? `${window.location.origin}${mediaURL({ media: data })}` : window.location.href; + const canonicalUrl = data ? `${window.location.origin}${mediaURL({ id: data.mediaid, title: data.title })}` : window.location.href; useEffect(() => { (document.scrollingElement || document.body).scroll({ top: 0 }); diff --git a/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx b/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx index 1797c7267..810925f4d 100644 --- a/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/playlistScreens/PlaylistGrid/PlaylistGrid.tsx @@ -32,7 +32,7 @@ const PlaylistGrid: ScreenComponent = ({ data, isLoading }) => { const pageTitle = `${data.title} - ${config.siteName}`; - const getUrl = (playlistItem: PlaylistItem) => mediaURL({ media: playlistItem, playlistId: playlistItem.feedid }); + const getUrl = (playlistItem: PlaylistItem) => mediaURL({ id: playlistItem.mediaid, title: playlistItem.title, playlistId: playlistItem.feedid }); return (
    diff --git a/packages/ui-react/src/pages/Search/Search.tsx b/packages/ui-react/src/pages/Search/Search.tsx index 30004fac1..19b699850 100644 --- a/packages/ui-react/src/pages/Search/Search.tsx +++ b/packages/ui-react/src/pages/Search/Search.tsx @@ -36,7 +36,8 @@ const Search = () => { const getURL = (playlistItem: PlaylistItem) => mediaURL({ - media: playlistItem, + id: playlistItem.mediaid, + title: playlistItem.title, playlistId: features?.searchPlaylist, }); From 0e5af7713b6759782cf23faa24184c723720acc5 Mon Sep 17 00:00:00 2001 From: Kire Mitrov Date: Thu, 26 Sep 2024 15:12:13 +0200 Subject: [PATCH 12/16] Fix / Access bridge tests (#623) * chore: override console in logger test --- knip.config.ts | 1 + .../access-bridge/test/unit/logger.test.ts | 17 +++++++++++++++++ platforms/access-bridge/vite.config.ts | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/knip.config.ts b/knip.config.ts index e32756a97..1d22ab1ee 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -38,6 +38,7 @@ const config: KnipConfig = { 'i18next-parser', 'luxon', // Used in tests 'playwright', // Used in test configs + 'sharp', // Requirement for @vite-pwa/assets-generator 'tsconfig-paths', // Used for e2e test setup 'virtual:pwa-register', // Service Worker code is injected at build time 'virtual:polyfills', // Polyfills are conditionally injected diff --git a/platforms/access-bridge/test/unit/logger.test.ts b/platforms/access-bridge/test/unit/logger.test.ts index a12c386a9..ae20d0d74 100644 --- a/platforms/access-bridge/test/unit/logger.test.ts +++ b/platforms/access-bridge/test/unit/logger.test.ts @@ -4,9 +4,26 @@ import * as Sentry from '@sentry/node'; import logger from '../../src/pipeline/logger.js'; describe('Logger Tests', () => { + // Preserve the original console methods + const originalConsole = { ...console }; + beforeEach(() => { // Reset all mocks to ensure a clean slate for each test vi.resetAllMocks(); + + // Mock console methods to suppress log outputs during tests + // Suppressing info output to avoid clutter + global.console = { + log: () => {}, + error: () => {}, + warn: () => {}, + info: () => {}, + } as unknown as Console; + }); + + afterEach(() => { + // Restore the original console methods after each test + global.console = originalConsole; }); describe('when Sentry is configured', () => { diff --git a/platforms/access-bridge/vite.config.ts b/platforms/access-bridge/vite.config.ts index c671b75ca..a755490e7 100644 --- a/platforms/access-bridge/vite.config.ts +++ b/platforms/access-bridge/vite.config.ts @@ -48,7 +48,7 @@ export default ({ mode, command }: ConfigEnv): UserConfigExport => { }, test: { globals: true, - include: ['**/*.test.ts'], + environment: 'node', setupFiles: 'test/vitest.setup.ts', chaiConfig: { truncateThreshold: 1000, From 5231d86b1aa05f6424fd8335f805fb42794d2698 Mon Sep 17 00:00:00 2001 From: Anton Lantukh Date: Thu, 26 Sep 2024 17:23:40 +0200 Subject: [PATCH 13/16] fix(e2e): fix cleeng tests (#624) --- platforms/web/test-e2e/tests/offers/choose_offer_test.ts | 1 + platforms/web/test-e2e/tests/payments/payments_test.ts | 8 ++++---- platforms/web/test-e2e/utils/constants.ts | 2 +- platforms/web/test/types.ts | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/platforms/web/test-e2e/tests/offers/choose_offer_test.ts b/platforms/web/test-e2e/tests/offers/choose_offer_test.ts index 5a7b4fb52..66ea117e6 100644 --- a/platforms/web/test-e2e/tests/offers/choose_offer_test.ts +++ b/platforms/web/test-e2e/tests/offers/choose_offer_test.ts @@ -16,6 +16,7 @@ const jwProps: ProviderProps = { fieldWrapper: '', hasInlineOfferSwitch: true, }; + const cleengProps: ProviderProps = { config: testConfigs.svod, monthlyOffer: constants.offers.monthlyOffer.cleeng, diff --git a/platforms/web/test-e2e/tests/payments/payments_test.ts b/platforms/web/test-e2e/tests/payments/payments_test.ts index e717954b5..07096115b 100644 --- a/platforms/web/test-e2e/tests/payments/payments_test.ts +++ b/platforms/web/test-e2e/tests/payments/payments_test.ts @@ -15,6 +15,7 @@ const jwProps: ProviderProps = { canRenewSubscription: false, fieldWrapper: '', hasInlineOfferSwitch: true, + cardInfo: Array.of(['Card number', '•••• •••• •••• 1111'], ['Expiry date', '03/2030'], ['Security code', '******']), }; const cleengProps: ProviderProps = { @@ -27,6 +28,7 @@ const cleengProps: ProviderProps = { canRenewSubscription: true, fieldWrapper: 'iframe', hasInlineOfferSwitch: false, + cardInfo: Array.of(['Card number', '•••• •••• •••• 1115'], ['Expiry date', '03/2030'], ['Security code', '******']), }; runTestSuite(jwProps, 'JW Player'); @@ -37,8 +39,6 @@ function runTestSuite(props: ProviderProps, providerName: string) { const today = new Date(); - const cardInfo = Array.of(['Card number', '•••• •••• •••• 1111'], ['Expiry date', '03/2030'], ['Security code', '******']); - Feature(`payments - ${providerName}`).retry(Number(process.env.TEST_RETRY_COUNT) || 0); Before(async ({ I }) => { @@ -84,7 +84,7 @@ function runTestSuite(props: ProviderProps, providerName: string) { await checkSubscription(I, addYear(today), today, props.yearlyOffer.price, props.hasInlineOfferSwitch); - cardInfo.forEach(([label, value]) => I.seeInField(label, value)); + props.cardInfo?.forEach(([label, value]) => I.seeInField(label, value)); }); Scenario(`I can cancel my subscription - ${providerName}`, async ({ I }) => { @@ -93,7 +93,7 @@ function runTestSuite(props: ProviderProps, providerName: string) { await cancelPlan(I, addYear(today), props.canRenewSubscription, providerName); // Still see payment info - cardInfo.forEach(([label, value]) => I.seeInField(label, value)); + props.cardInfo?.forEach(([label, value]) => I.seeInField(label, value)); }); Scenario(`I can renew my subscription - ${providerName}`, async ({ I }) => { diff --git a/platforms/web/test-e2e/utils/constants.ts b/platforms/web/test-e2e/utils/constants.ts index fbc18d5c9..88c215b00 100644 --- a/platforms/web/test-e2e/utils/constants.ts +++ b/platforms/web/test-e2e/utils/constants.ts @@ -101,6 +101,6 @@ export default { }, creditCard: { inplayer: '4111111111111111', - cleeng: '5555444433331111', + cleeng: '5555341244441115', }, }; diff --git a/platforms/web/test/types.ts b/platforms/web/test/types.ts index 5eb66a16f..0b14f09c6 100644 --- a/platforms/web/test/types.ts +++ b/platforms/web/test/types.ts @@ -26,4 +26,5 @@ export type ProviderProps = { locale?: string | undefined; fieldWrapper?: string; hasInlineOfferSwitch: boolean; + cardInfo?: string[][]; }; From 003e3e505929d62f3547c45def885a1b32b32e82 Mon Sep 17 00:00:00 2001 From: Roy Schut Date: Fri, 27 Sep 2024 14:13:28 +0200 Subject: [PATCH 14/16] fix(project): demo config reset doesnt work (#609) refactor(project): remove unnecessary form target refactor(project): update snapshot --- .../DemoConfigDialog/DemoConfigDialog.tsx | 21 +++---------------- .../DemoConfigDialog.test.tsx.snap | 1 - 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/platforms/web/src/components/DemoConfigDialog/DemoConfigDialog.tsx b/platforms/web/src/components/DemoConfigDialog/DemoConfigDialog.tsx index 29bad61f0..84b270ddd 100644 --- a/platforms/web/src/components/DemoConfigDialog/DemoConfigDialog.tsx +++ b/platforms/web/src/components/DemoConfigDialog/DemoConfigDialog.tsx @@ -1,6 +1,5 @@ import React, { type ChangeEventHandler, type MouseEventHandler, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { type NavigateFunction, useNavigate } from 'react-router'; import { Helmet } from 'react-helmet'; import { createURL } from '@jwp/ott-common/src/utils/urlFormatting'; import { CONFIG_QUERY_KEY } from '@jwp/ott-common/src/constants'; @@ -12,6 +11,7 @@ import LoadingOverlay from '@jwp/ott-ui-react/src/components/LoadingOverlay/Load import DevStackTrace from '@jwp/ott-ui-react/src/components/DevStackTrace/DevStackTrace'; import type { BootstrapData } from '@jwp/ott-hooks-react/src/useBootstrapApp'; import { AppError } from '@jwp/ott-common/src/utils/error'; +import { PATH_HOME } from '@jwp/ott-common/src/paths'; import styles from './DemoConfigDialog.module.scss'; @@ -32,26 +32,11 @@ const initialState: State = { loaded: false, }; -export function getConfigNavigateCallback(navigate: NavigateFunction) { - return (configSource: string) => { - navigate( - { - pathname: '/', - search: new URLSearchParams([[CONFIG_QUERY_KEY, configSource]]).toString(), - }, - { replace: true }, - ); - }; -} - const DemoConfigDialog = ({ query }: { query: BootstrapData }) => { const { data, isLoading, error, refetch, isSuccess } = query; const { configSource: selectedConfigSource } = data || {}; const { t } = useTranslation('demo'); - const navigate = useNavigate(); - const navigateCallback = getConfigNavigateCallback(navigate); - const [state, setState] = useState(initialState); const errorTitle = error && error instanceof AppError ? error.payload.title : ''; @@ -91,7 +76,7 @@ const DemoConfigDialog = ({ query }: { query: BootstrapData }) => { const clearConfig = () => { setState(initialState); - navigateCallback(''); + window.location.href = createURL(PATH_HOME, { [CONFIG_QUERY_KEY]: '' }); }; const isValidSource = (configSource: string) => configSource.match(regex)?.some((m) => m === configSource); @@ -170,7 +155,7 @@ const DemoConfigDialog = ({ query }: { query: BootstrapData }) => { helpLink={'https://docs.jwplayer.com/platform/docs/ott-create-an-app-config'} error={typeof state.error === 'string' ? undefined : state.error} > -
    + > renders and matches snapshot error dialog 1`] = `

    Date: Fri, 27 Sep 2024 14:27:02 +0200 Subject: [PATCH 15/16] feat(project): screen animations (#614) * feat: add screen animations * chore: update snapshots * fix: animation on mount not stable * chore: remove key from media screen routing --- .../src/components/Animation/Animation.tsx | 8 +- .../src/components/Header/Header.module.scss | 3 +- .../src/containers/ShelfList/ShelfList.tsx | 31 +- .../Home/__snapshots__/Home.test.tsx.snap | 304 +++++++++--------- .../pages/ScreenRouting/MediaScreenRouter.tsx | 7 +- .../ScreenRouting/PlaylistScreenRouter.tsx | 11 +- platforms/web/src/styles/main.scss | 2 +- 7 files changed, 197 insertions(+), 169 deletions(-) diff --git a/packages/ui-react/src/components/Animation/Animation.tsx b/packages/ui-react/src/components/Animation/Animation.tsx index 50f94bf4a..250de7d91 100644 --- a/packages/ui-react/src/components/Animation/Animation.tsx +++ b/packages/ui-react/src/components/Animation/Animation.tsx @@ -15,6 +15,8 @@ type Props = { export type Status = 'opening' | 'open' | 'closing' | 'closed'; +const triggerReflow = (element: HTMLElement | null) => element?.scrollTop; + const Animation: React.FC = ({ className, createStyle, @@ -26,6 +28,7 @@ const Animation: React.FC = ({ keepMounted = false, children, }) => { + const nodeRef = useRef(null); const [status, setStatus] = useState('closed'); const [hasOpenedBefore, setHasOpenedBefore] = useState(false); @@ -35,6 +38,9 @@ const Animation: React.FC = ({ // use event callbacks to ignore reactive dependencies const openEvent = useEventCallback(() => { setHasOpenedBefore(true); + // trigger a reflow to ensure the transition is respected after mount + triggerReflow(nodeRef.current); + timeout.current = window.setTimeout(() => setStatus('opening'), delay); timeout2.current = window.setTimeout(() => { setStatus('open'); @@ -70,7 +76,7 @@ const Animation: React.FC = ({ } return ( -
    +
    {children}
    ); diff --git a/packages/ui-react/src/components/Header/Header.module.scss b/packages/ui-react/src/components/Header/Header.module.scss index 90ccf3e50..0944cf960 100644 --- a/packages/ui-react/src/components/Header/Header.module.scss +++ b/packages/ui-react/src/components/Header/Header.module.scss @@ -27,7 +27,8 @@ // Make header static // &.static { - position: static; + position: relative; + z-index: 1; width: 100%; } } diff --git a/packages/ui-react/src/containers/ShelfList/ShelfList.tsx b/packages/ui-react/src/containers/ShelfList/ShelfList.tsx index de19fb526..e9097919b 100644 --- a/packages/ui-react/src/containers/ShelfList/ShelfList.tsx +++ b/packages/ui-react/src/containers/ShelfList/ShelfList.tsx @@ -16,6 +16,7 @@ import usePlaylists from '@jwp/ott-hooks-react/src/usePlaylists'; import Shelf from '../../components/Shelf/Shelf'; import InfiniteScrollLoader from '../../components/InfiniteScrollLoader/InfiniteScrollLoader'; import ErrorPage from '../../components/ErrorPage/ErrorPage'; +import Fade from '../../components/Animation/Fade/Fade'; import styles from './ShelfList.module.scss'; @@ -76,20 +77,22 @@ const ShelfList = ({ rows }: Props) => { data-testid={testId(`shelf-${featured ? 'featured' : type === 'playlist' ? slugify(title || playlist?.title) : type}`)} aria-label={title || playlist?.title} > - + + + ); })} diff --git a/packages/ui-react/src/pages/Home/__snapshots__/Home.test.tsx.snap b/packages/ui-react/src/pages/Home/__snapshots__/Home.test.tsx.snap index 27e966e86..ca0219352 100644 --- a/packages/ui-react/src/pages/Home/__snapshots__/Home.test.tsx.snap +++ b/packages/ui-react/src/pages/Home/__snapshots__/Home.test.tsx.snap @@ -19,111 +19,115 @@ exports[`Home Component tests > Home test 1`] = ` data-testid="shelf-this-is-a-playlist" > - +
    @@ -134,111 +138,115 @@ exports[`Home Component tests > Home test 1`] = ` data-testid="shelf-second-playlist" > - +
    diff --git a/packages/ui-react/src/pages/ScreenRouting/MediaScreenRouter.tsx b/packages/ui-react/src/pages/ScreenRouting/MediaScreenRouter.tsx index fcb70b9f6..65bc5db2e 100644 --- a/packages/ui-react/src/pages/ScreenRouting/MediaScreenRouter.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/MediaScreenRouter.tsx @@ -10,6 +10,7 @@ import useMedia from '@jwp/ott-hooks-react/src/useMedia'; import type { ScreenComponent } from '../../../types/screens'; import Loading from '../Loading/Loading'; import ErrorPage from '../../components/ErrorPage/ErrorPage'; +import Fade from '../../components/Animation/Fade/Fade'; import MediaStaticPage from './mediaScreens/MediaStaticPage/MediaStaticPage'; import MediaMovie from './mediaScreens/MediaMovie/MediaMovie'; @@ -48,7 +49,11 @@ const MediaScreenRouter = () => { const MediaScreen = mediaScreenMap.getScreen(data); - return ; + return ( + + + + ); }; export default MediaScreenRouter; diff --git a/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx b/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx index 1663b49a3..e52ea9298 100644 --- a/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx +++ b/packages/ui-react/src/pages/ScreenRouting/PlaylistScreenRouter.tsx @@ -10,6 +10,7 @@ import type { AppMenuType } from '@jwp/ott-common/types/config'; import Loading from '../Loading/Loading'; import ErrorPage from '../../components/ErrorPage/ErrorPage'; import type { ScreenComponent } from '../../../types/screens'; +import Fade from '../../components/Animation/Fade/Fade'; import PlaylistGrid from './playlistScreens/PlaylistGrid/PlaylistGrid'; import PlaylistLiveChannels from './playlistScreens/PlaylistLiveChannels/PlaylistLiveChannels'; @@ -28,10 +29,10 @@ const PlaylistScreenRouter = ({ type }: { type: AppMenuType }) => { const params = useParams(); const id = params.id || ''; - const { isLoading, isFetching, error, data } = usePlaylist(id, {}, true, true, type); + const { isFetching, error, data } = usePlaylist(id, {}, true, true, type); const { t } = useTranslation('error'); - if (isLoading) { + if (isFetching) { return ; } @@ -45,7 +46,11 @@ const PlaylistScreenRouter = ({ type }: { type: AppMenuType }) => { const Screen = type === APP_CONFIG_ITEM_TYPE.content_list ? contentScreenMap.getScreen(data) : playlistScreenMap.getScreen(data); - return ; + return ( + + + + ); }; export default PlaylistScreenRouter; diff --git a/platforms/web/src/styles/main.scss b/platforms/web/src/styles/main.scss index d1c89e698..bfaf19360 100644 --- a/platforms/web/src/styles/main.scss +++ b/platforms/web/src/styles/main.scss @@ -24,7 +24,7 @@ body { margin: 0; padding: 0; overflow-x: hidden; - overflow-y: auto; + overflow-y: scroll; // this prevents layout jumps when navigating between screens color: var(--body-color); font-family: var(--body-font-family); font-size: variables.$body-font-size; From 3c24a62dfb0a477954664f544f1aeb06bef36549 Mon Sep 17 00:00:00 2001 From: Conventional Changelog Action Date: Fri, 27 Sep 2024 12:29:04 +0000 Subject: [PATCH 16/16] chore(release): v6.7.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 457b54f7b..e5ec27ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [6.7.0](https://github.com/jwplayer/ott-web-app/compare/v6.6.0...v6.7.0) (2024-09-27) + + +### Features + +* access bridge service ([#616](https://github.com/jwplayer/ott-web-app/issues/616)) ([aae2551](https://github.com/jwplayer/ott-web-app/commit/aae25515bcff0e4ad8428d8b8389220eb9126d5f)), closes [#594](https://github.com/jwplayer/ott-web-app/issues/594) [#598](https://github.com/jwplayer/ott-web-app/issues/598) [#590](https://github.com/jwplayer/ott-web-app/issues/590) [#605](https://github.com/jwplayer/ott-web-app/issues/605) [#606](https://github.com/jwplayer/ott-web-app/issues/606) +* **i18n:** fix lint error ([c27a1e6](https://github.com/jwplayer/ott-web-app/commit/c27a1e69c360d9ebe9abcc9cf2e8bb3a088516aa)) +* **menu:** fix support for media menu item ([#621](https://github.com/jwplayer/ott-web-app/issues/621)) ([4ffb849](https://github.com/jwplayer/ott-web-app/commit/4ffb84923424de05f1f62455c7822354c0842173)) +* **menu:** support media type for menu ([#610](https://github.com/jwplayer/ott-web-app/issues/610)) ([80acd7f](https://github.com/jwplayer/ott-web-app/commit/80acd7f6c6672baf6e4a01dbecdf8540be68dbd7)) +* **project:** screen animations ([#614](https://github.com/jwplayer/ott-web-app/issues/614)) ([edbb246](https://github.com/jwplayer/ott-web-app/commit/edbb246c9edf926b872f5fdeb01d8cc0379f86ae)) + + +### Bug Fixes + +* card grid rendering previous items ([#613](https://github.com/jwplayer/ott-web-app/issues/613)) ([dce9f70](https://github.com/jwplayer/ott-web-app/commit/dce9f70875444ad965bb69d87a3f9445883ab9b1)) +* **e2e:** fix cleeng tests ([#624](https://github.com/jwplayer/ott-web-app/issues/624)) ([5231d86](https://github.com/jwplayer/ott-web-app/commit/5231d86b1aa05f6424fd8335f805fb42794d2698)) +* **project:** demo config reset doesnt work ([#609](https://github.com/jwplayer/ott-web-app/issues/609)) ([003e3e5](https://github.com/jwplayer/ott-web-app/commit/003e3e505929d62f3547c45def885a1b32b32e82)) +* **series:** first episode switching per season ([ddedb6b](https://github.com/jwplayer/ott-web-app/commit/ddedb6b6f636c720f0ba016b2fa07b41e964fe8e)) +* update plan types and access bridge port for the unit test workflow ([#619](https://github.com/jwplayer/ott-web-app/issues/619)) ([0e51bc4](https://github.com/jwplayer/ott-web-app/commit/0e51bc44288fb1ab2cd55dc8bdeaec8233819043)) + ## [6.6.0](https://github.com/jwplayer/ott-web-app/compare/v6.5.0...v6.6.0) (2024-09-06) diff --git a/package.json b/package.json index 85821ed50..ae325d03c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jwp/ott", - "version": "6.6.0", + "version": "6.7.0", "private": true, "license": "Apache-2.0", "repository": "https://github.com/jwplayer/ott-web-app.git",