diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dd22fb..d7f2d70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,17 @@ jobs: docker: if: ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.DOCKER_PASSWORD }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 diff --git a/.gitignore b/.gitignore index a2f3de4..69ef532 100644 --- a/.gitignore +++ b/.gitignore @@ -1,78 +1,105 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + # Logs + logs -*.log -npm-debug.log* +_.log +npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* +# Caches + +.cache + # Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data + pids -*.pid -*.seed +_.pid +_.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover + lib-cov # Coverage directory used by tools like istanbul + coverage *.lcov # nyc test coverage + .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + .grunt # Bower dependency directory (https://bower.io/) + bower_components # node-waf configuration + .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) + build/Release # Dependency directories + node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) + web_modules/ # TypeScript cache + *.tsbuildinfo # Optional npm cache directory + .npm # Optional eslint cache + .eslintcache # Optional stylelint cache + .stylelintcache # Microbundle cache + .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history + .node_repl_history # Output of 'npm pack' + *.tgz # Yarn Integrity file + .yarn-integrity # dotenv environment variable files + .env .env.development.local .env.test.local @@ -80,55 +107,71 @@ web_modules/ .env.local # parcel-bundler cache (https://parceljs.org/) -.cache + .parcel-cache # Next.js build output + .next out # Nuxt.js build / generate output + .nuxt dist # Gatsby files -.cache/ + # Comment in the public line in if your project uses Gatsby and not Next.js + # https://nextjs.org/blog/next-9-1#public-directory-support + # public # vuepress build output + .vuepress/dist # vuepress v2.x temp and cache directory + .temp -.cache # Docusaurus cache and generated files + .docusaurus # Serverless directories + .serverless/ # FuseBox cache + .fusebox/ # DynamoDB Local files + .dynamodb/ # TernJS port file + .tern-port # Stores VSCode versions used for testing VSCode extensions + .vscode-test # yarn v2 + .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* -s3.json -yt-dlp -.DS_Store \ No newline at end of file +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +s3.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3f42341..dddd5cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM node:alpine +FROM oven/bun:1 AS base RUN mkdir -p /usr/src/preservetube/backend WORKDIR /usr/src/preservetube/backend COPY . /usr/src/preservetube/backend -RUN yarn +RUN bun install -CMD ["node", "index.js"] \ No newline at end of file +CMD ["bun", "run", "src/index.ts"] \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 29ebfa5..0000000 --- a/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..f959ebb Binary files /dev/null and b/bun.lockb differ diff --git a/controller/latest.js b/controller/latest.js deleted file mode 100644 index 56a1592..0000000 --- a/controller/latest.js +++ /dev/null @@ -1,102 +0,0 @@ -const { PrismaClient } = require('@prisma/client') -const redis = require('../utils/redis.js') -const prisma = new PrismaClient() - -function createSitemapXML(urls) { - const xml = urls.map(url => ` - - ${url} - never - 0.7 - - `).join(''); - - return ` - - ${xml} - `; -} - -function createSitemapIndexXML(sitemaps) { - const xml = sitemaps.map((sitemap, index) => ` - - https://api.preservetube.com/${sitemap} - ${new Date().toISOString()} - - `).join(''); - - return ` - - ${xml} - `; -} - -exports.getLatest = async (req, res) => { - let json - const cached = await redis.get('latest') - - if (cached) { - json = JSON.parse(cached) - } else { - json = await prisma.videos.findMany({ - take: 90, - orderBy: [ - { - archived: 'desc' - } - ], - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true, - channel: true, - channelId: true, - channelAvatar: true, - channelVerified: true - } - }) - await redis.set('latest', JSON.stringify(json), 'EX', 3600) - } - - res.json(json) -} - -exports.getSitemap = async (req, res) => { - const cachedSitemapIndex = await redis.get('sitemap-index'); - if (cachedSitemapIndex) { - res.header('Content-Type', 'application/xml'); - return res.send(cachedSitemapIndex); - } - - const dbVideos = await prisma.videos.findMany({ - select: { - id: true, - }, - }); - - const urls = dbVideos.map((video) => `https://preservetube.com/watch?v=${video.id}`); - const sitemaps = []; - for (let i = 0; i < urls.length; i += 50000) { - const batch = urls.slice(i, i + 50000); - await redis.set(`sitemap-${sitemaps.length}`, createSitemapXML(batch), 'EX', 86400); - sitemaps.push(`sitemap-${sitemaps.length}.xml`); - } - - const sitemapIndexXML = createSitemapIndexXML(sitemaps); - await redis.set('sitemap-index', sitemapIndexXML, 'EX', 86400); - - res.header('Content-Type', 'application/xml'); - res.send(sitemapIndexXML); -}; - -exports.getSubSitemap = async (req, res) => { - const cachedSitemap = await redis.get(`sitemap-${req.params.index}`); - if (cachedSitemap) { - res.header('Content-Type', 'application/xml'); - return res.send(cachedSitemap); - } - - res.status(404).send(''); -}; \ No newline at end of file diff --git a/controller/search.js b/controller/search.js deleted file mode 100644 index 6b5e18a..0000000 --- a/controller/search.js +++ /dev/null @@ -1,52 +0,0 @@ -const crypto = require('node:crypto') -const validate = require('../utils/validate.js') -const redis = require('../utils/redis.js') -const { RedisRateLimiter } = require('rolling-rate-limiter') -const { PrismaClient } = require('@prisma/client') -const prisma = new PrismaClient() - -const limiter = new RedisRateLimiter({ - client: redis, - namespace: 'search:', - interval: 5 * 60 * 1000, - maxInInterval: 5 -}) - -exports.searchVideo = async (req, res) => { - const ipHash = crypto.createHash('sha256').update(req.headers['x-userip'] || '0.0.0.0').digest('hex') - const isLimited = await limiter.limit(ipHash) - if (isLimited) return res.status(429).send('error-You have been ratelimited.') - - const id = await validate.validateVideoInput(req.query.search) - if (id.fail) { - const videos = await prisma.videos.findMany({ - where: { - title: { - contains: req.query.search, - mode: 'insensitive' - } - } - }) - res.json(videos) - } else { - res.send(`redirect-${process.env.FRONTEND}/watch?v=${id}`) - } -} - -exports.searchPlaylist = async (req, res) => { - const id = await validate.validatePlaylistInput(req.query.url) - if (id.fail) { - res.status(500).send(id.message) - } else { - res.redirect(`${process.env.FRONTEND}/playlist?list=${id}`) - } -} - -exports.searchChannel = async (req, res) => { - const id = await validate.validateChannelInput(req.query.url) - if (id.fail) { - res.status(500).send(id.message) - } else { - res.redirect(`${process.env.FRONTEND}/channel/${id}`) - } -} \ No newline at end of file diff --git a/controller/transparency.js b/controller/transparency.js deleted file mode 100644 index 2b581fa..0000000 --- a/controller/transparency.js +++ /dev/null @@ -1,32 +0,0 @@ -const { PrismaClient } = require('@prisma/client') -const redis = require('../utils/redis.js') -const prisma = new PrismaClient() - -exports.getReports = async (req, res) => { - let json - const cached = await redis.get('transparency') - - if (cached) { - json = JSON.parse(cached) - } else { - json = (await prisma.reports.findMany()).map(r => { - return { - ...r, - details: (r.details).split('<').join('<').split('>').join('>'), - date: (r.date).toISOString().slice(0,10) - } - }) - await redis.set('transparency', JSON.stringify(json), 'EX', 3600) - } - - res.json(json) -} - -exports.getReports = async (req, res) => { - const reports = await prisma.reports.findMany({ - where: { - target: req.params.id - } - }) - res.json(reports) -} \ No newline at end of file diff --git a/controller/video.js b/controller/video.js deleted file mode 100644 index d1985c3..0000000 --- a/controller/video.js +++ /dev/null @@ -1,200 +0,0 @@ -const { PrismaClient } = require('@prisma/client') -const prisma = new PrismaClient() - -const DOMPurify = require('isomorphic-dompurify') -const rtm = require('readable-to-ms') -const metadata = require('../utils/metadata.js') -const redis = require('../utils/redis.js') - -exports.getVideo = async (req, res) => { - let info - const cached = await redis.get(`video:${req.params.id}`) - - if (cached) { - info = JSON.parse(cached) - } else { - info = await prisma.videos.findFirst({ - where: { - id: req.params.id - }, - select: { - title: true, - description: true, - thumbnail: true, - source: true, - published: true, - archived: true, - channel: true, - channelId: true, - channelAvatar: true, - channelVerified: true, - disabled: true, - hasBeenReported: true - } - }) - - if (!info) return res.json({ error: '404' }) - await redis.set(`video:${req.params.id}`, JSON.stringify(info), 'EX', 3600) - } - - res.json({ - ...info, - description: DOMPurify.sanitize(info.description), - }) -} - -exports.getChannel = async (req, res) => { - const cached = await redis.get(`channel:${req.params.id}`) - if (cached) return res.json(JSON.parse(cached)) - - const [videos, channel] = await Promise.all([ - metadata.getChannelVideos(req.params.id), - metadata.getChannel(req.params.id) - ]) - - if (!videos || !channel || videos.error || channel.error) { - return res.json({ error: '404' }); - } - - const archived = await prisma.videos.findMany({ - where: { - channelId: req.params.id - }, - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true - } - }) - - const processedVideos = videos.map(video => { - const date = !isNaN(new Date(video.published.text).getTime()) ? new Date(video.published.text) : new Date((new Date()).getTime() - rtm(video.published.text).ms); // life is great. - return { - id: video.id, - title: video.title.text, - thumbnail: video.thumbnails[0].url, - published: new Date(date).toISOString().slice(0, 10) - } - }); - - archived.forEach(v => { - const existingVideoIndex = processedVideos.findIndex(video => video.id === v.id); - if (existingVideoIndex !== -1) { - processedVideos[existingVideoIndex] = v; - } else { - processedVideos.push({ ...v, deleted: undefined }); - } - }); - - processedVideos.sort((a, b) => new Date(b.published) - new Date(a.published)); - - const json = { - name: channel.metadata.title, - avatar: channel.metadata.avatar[0].url, - verified: channel.header.author?.is_verified, - videos: processedVideos - } - await redis.set(`channel:${req.params.id}`, JSON.stringify(json), 'EX', 3600) - res.json(json) -} - -exports.getOnlyChannelVideos = async (req, res) => { - const cached = await redis.get(`channelVideos:${req.params.id}`) - if (cached) return res.json(JSON.parse(cached)) - - const archived = await prisma.videos.findMany({ - where: { - channelId: req.params.id - }, - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true - }, - orderBy: { - published: 'desc' - } - }) - - const json = { - videos: archived - } - await redis.set(`channelVideos:${req.params.id}`, JSON.stringify(json), 'EX', 3600) - res.json(json) -} - -exports.getPlaylist = async (req, res) => { - const cached = await redis.get(`playlist:${req.params.id}`) - if (cached) return res.json(JSON.parse(cached)) - - const playlist = await metadata.getPlaylistVideos(req.params.id) - if (!playlist || playlist.error) return res.json({ error: '404' }) - - const playlistArchived = await prisma.videos.findMany({ - where: { - playlist: req.params.id - }, - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true - } - }) - - const allVideos = playlist.relatedStreams.map(video => ({ - id: video.url.replace('/watch?v=', ''), - published: new Date(video.uploaded).toISOString().slice(0, 10), - ...video - })); - - await Promise.all(playlistArchived.map(async (v) => { - const allVideo = allVideos.find(o => o.id == v.id); - if (allVideo) { - const index = allVideos.findIndex(o => o.id == v.id); - allVideos[index] = v; - } else { - const live = await metadata.getVideoMetadata(v.id); - allVideos.push({ - ...v, - deleted: live.error ? true : false - }); - } - })); - - await Promise.all(allVideos.filter(v => !v.archived).map(async (v) => { - const video = await prisma.videos.findFirst({ - where: { - id: v.id - }, - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true - } - }); - if (video) { - const index = allVideos.findIndex(o => o.id == v.id); - allVideos[index] = video; - } - })); - - allVideos.sort((a, b) => new Date(b.published) - new Date(a.published)); - - const json = { - name: playlist.name, - channel: playlist.uploader, - url: playlist.uploaderUrl, - avatar: playlist.uploaderAvatar, - videos: allVideos - } - await redis.set(`playlist:${req.params.id}`, JSON.stringify(json), 'EX', 3600) - res.json(json) -} \ No newline at end of file diff --git a/controller/websocket.js b/controller/websocket.js deleted file mode 100644 index 4674c4c..0000000 --- a/controller/websocket.js +++ /dev/null @@ -1,335 +0,0 @@ -const fs = require('node:fs') -const crypto = require('node:crypto') -const { RedisRateLimiter } = require('rolling-rate-limiter') -const rtm = require('readable-to-ms') - -const upload = require('../utils/upload.js') -const ytdlp = require('../utils/ytdlp.js') -const redis = require('../utils/redis.js') - -const validate = require('../utils/validate.js') -const metadata = require('../utils/metadata.js') -const websocket = require('../utils/websocket.js') -const captcha = require("../utils/captcha.js") -const logger = require("../utils/logger.js") - -const { PrismaClient } = require('@prisma/client') -const prisma = new PrismaClient() - -const limiter = new RedisRateLimiter({ - client: redis, - namespace: 'autodownload:', - interval: 24 * 60 * 60 * 1000, - maxInInterval: 5 -}) - -exports.save = async (ws, req) => { - logger.info({ message: `${req.path} ${JSON.stringify(req.query)}` }) - - const id = await validate.validateVideoInput(req.query.url) - if (id.fail) { - ws.send(`ERROR - ${id.message}`) - return ws.close() - } - - if (await redis.get(id)) { - ws.send('DATA - Someone is already downloading this video...') - return ws.close() - } - - if (await redis.get(`blacklist:${id}`)) { - ws.send('DATA - You can\'t download that. The video is blacklisted.') - return ws.close() - } - - const already = await prisma.videos.findFirst({ - where: { - id: id - } - }) - - if (already) return ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${id}`) - - ws.send('DATA - This process is automatic. Your video will start archiving shortly.') - ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') - - ws.on('message', async function(msg) { - if (msg == 'alive') return - - if (await redis.get(id) != 'downloading') { - await redis.set(id, 'downloading', 'EX', 300) - const confirm = await captcha.checkCaptcha(msg) - - if (confirm) startDownloading() - else { - await redis.del(id) - ws.send('DATA - You little goofy goober tried to mess with the captcha...') - ws.close() - } - } else { - ws.send('DATA - You already sent captcha reply...') - } - }) - - async function startDownloading() { - const download = await ytdlp.downloadVideo(`https://www.youtube.com/watch?v=${id}`, ws, id) - if (download.fail) { - await redis.del(id) - ws.send(`DATA - ${download.message}`) - ws.close() - } else { - const file = fs.readdirSync("videos").find(f => f.includes(id)) - if (file) { - ws.send('DATA - Uploading file...') - const videoUrl = await upload.uploadVideo(`./videos/${id}.mp4`) - fs.unlinkSync(`./videos/${id}.mp4`) - - const uploaded = await websocket.createDatabaseVideo(id, videoUrl) - if (uploaded != 'success') { - ws.send(`DATA - Error while uploading - ${JSON.stringify(uploaded)}`) - } else { - ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${id}`) - } - } - - await redis.del(id) - ws.close(); - } - } -} - -exports.playlist = async (ws, req) => { - logger.info({ message: `${req.path} ${JSON.stringify(req.query)}` }) - - const playlistId = await validate.validatePlaylistInput(req.query.url) - if (playlistId.fail) { - ws.send(`ERROR - ${playlistId.message}`) - return ws.close() - } - - let status = 'captcha' - ws.send('DATA - This process is automatic. Your video will start archiving shortly.') - ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') - - ws.on('message', async function(msg) { - if (msg == 'alive') return - - if (status == 'captcha') { - status = 'downloading' - const confirm = await captcha.checkCaptcha(msg) - - if (confirm) startDownloading() - else { - await redis.del(id) - ws.send('DATA - You little goofy goober tried to mess with the captcha...') - ws.close() - } - } else { - ws.send('DATA - You already sent captcha reply...') - } - }) - - async function startDownloading() { - const playlist = await metadata.getPlaylistVideos(playlistId) - for (video of playlist.relatedStreams.slice(0, 5)) { - if (ws.readyState !== ws.OPEN) { - return logger.info({ message: `Stopped downloading ${playlistId}, websocket is closed` }) - } - - const id = video.url.match(/[?&]v=([^&]+)/)[1] - - const already = await prisma.videos.findFirst({ - where: { - id: id - } - }) - - if (already) { - ws.send(`DATA - Already downloaded ${video.title}`) - await prisma.videos.updateMany({ - where: { - id: id - }, - data: { - playlist: playlistId - } - }) - continue - } - - if (await redis.get(id)) { - ws.send(`DATA - Someone is already downloading ${video.title}, skipping.`) - continue - } - - if (await redis.get(`blacklist:${id}`)) { - ws.send(`DATA - ${video.title} is blacklisted from downloading, skipping`) - continue - } - - ws.send(`INFO - Downloading ${video.title}

`) - await redis.set(id, 'downloading', 'EX', 300) - - const download = await ytdlp.downloadVideo('https://www.youtube.com' + video.url, ws, id) - if (download.fail) { - ws.send(`DATA - ${download.message}`) - await redis.del(id) - continue - } else { - const file = fs.readdirSync("./videos").find(f => f.includes(id)) - if (file) { - try { - ws.send(`DATA - Downloaded ${video.title}`) - ws.send(`DATA - Uploading ${video.title}`) - - const videoUrl = await upload.uploadVideo(`./videos/${id}.mp4`) - ws.send(`DATA - Uploaded ${video.title}`) - fs.unlinkSync(`./videos/${id}.mp4`) - - await websocket.createDatabaseVideo(id, videoUrl, playlistId) - ws.send(`DATA - Created video page for ${video.title}`) - } catch (e) { - ws.send(`DATA - Failed downloading video ${video.title}. Going to next video`) - logger.error(e) - } - } else { - ws.send(`DATA - Failed to find file for ${video.title}. Going to next video in the playlist`) - } - - await redis.del(id) - } - } - - ws.send(`DONE - ${process.env.FRONTEND}/playlist?list=${playlistId}`) - } -} - -exports.channel = async (ws, req) => { - logger.info({ message: `${req.path} ${JSON.stringify(req.query)}` }) - - const channelId = await validate.validateChannelInput(req.query.url) - if (channelId.fail) { - ws.send(`ERROR - ${channelId.message}`) - return ws.close() - } - - let status = 'captcha' - ws.send('DATA - This process is automatic. Your video will start archiving shortly.') - ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') - - ws.on('message', async function(msg) { - if (msg == 'alive') return - - if (status == 'captcha') { - status = 'downloading' - const confirm = await captcha.checkCaptcha(msg) - - if (confirm) startDownloading() - else { - ws.send('DATA - You little goofy goober tried to mess with the captcha...') - ws.close() - } - } else { - ws.send('DATA - You already sent captcha reply...') - } - }) - - async function startDownloading() { - const videos = await metadata.getChannelVideos(channelId) - - for (const video of videos.slice(0, 5)) { - if (ws.readyState !== ws.OPEN) { - return logger.info({ message: `Stopped downloading ${channelId}, websocket is closed` }) - } - - const already = await prisma.videos.findFirst({ - where: { - id: video.id - } - }) - - if (already) { - ws.send(`DATA - Already downloaded ${video.title.text}`) - continue - } - - if (await redis.get(video.id)) { - ws.send(`DATA - Someone is already downloading ${video.title.text}, skipping.`) - continue - } - - if (await redis.get(`blacklist:${video.id}`)) { - ws.send(`DATA - ${video.title.text} is blacklisted from downloading, skipping`) - continue - } - - ws.send(`INFO - Downloading ${video.title.text}

`) - await redis.set(video.id, 'downloading', 'EX', 300) - - const download = await ytdlp.downloadVideo(`https://www.youtube.com/watch?v=${video.id}`, ws, video.id) - if (download.fail) { - ws.send(`DATA - ${download.message}`) - await redis.del(video.id) - continue - } else { - const file = fs.readdirSync("./videos").find(f => f.includes(video.id)) - if (file) { - try { - ws.send(`DATA - Downloaded ${video.title.text}`) - ws.send(`DATA - Uploading ${video.title.text}`) - - const videoUrl = await upload.uploadVideo(`./videos/${video.id}.mp4`) - ws.send(`DATA - Uploaded ${video.title.text}`) - fs.unlinkSync(`./videos/${video.id}.mp4`) - - await websocket.createDatabaseVideo(video.id, videoUrl) - ws.send(`DATA - Created video page for ${video.title.text}`) - } catch (e) { - ws.send(`DATA - Failed downloading video ${video.title.text}. Going to next video`) - logger.error(e) - } - } else { - ws.send(`DATA - Failed to find file for ${video.title.text}. Going to next video`) - } - - await redis.del(video.id) - } - } - - ws.send(`DONE - ${process.env.FRONTEND}/channel/${channelId}`) - } -} - -exports.addAutodownload = async (req, res) => { - const confirm = await captcha.checkCaptcha(req.query.captcha) - if (!confirm) return res.status(500).send('You little goofy goober tried to mess with the captcha...') - - const channelId = await validate.validateChannelInput(req.query.url) - if (channelId.fail) { - return res.status(500).send(channelId.message) - } - - const already = await prisma.autodownload.findFirst({ - where: { - channel: channelId - } - }) - - if (already) { - res.status(500).send(`This channel is already being automatically downloaded...`) - } else { - const ipHash = crypto.createHash('sha256').update(req.headers['x-forwarded-for'] || req.connection.remoteAddress).digest('hex') - const isLimited = await limiter.limit(ipHash) - - if (isLimited) return res.status(420).send(`Hey! You have reached the limit of 5 queued auto-download channels per day. Sadly, hard drives don't grow on trees, so rate limits are necessary. The "Save Channel" feature has no limits, so feel free to use that.

- -Are you planning something awesome? Feel free to email me at admin[@]preservetube.com.`) - - await prisma.autodownload.create({ - data: { - channel: channelId - } - }) - res.send('Perfect! Each time this channel uploads their videos will be downloaded') - } -} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3d5ca4f..a940e87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,17 +6,13 @@ services: image: ghcr.io/preservetube/backend networks: - public - - db restart: on-failure volumes: - - ./logs:/usr/src/preservetube/backend/logs + - /mnt/hdd/preservetube-videos:/usr/src/preservetube/backend/videos - ./.env:/usr/src/preservetube/backend/.env - ./s3.json:/usr/src/preservetube/backend/s3.json networks: public: external: true - name: public - db: - external: true - name: db \ No newline at end of file + name: public \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index f1514e3..0000000 --- a/index.js +++ /dev/null @@ -1,47 +0,0 @@ -require('dotenv').config() - -const express = require('express') -const cors = require('cors') - -const logger = require('./utils/logger.js') - -const latestController = require('./controller/latest.js') -const videoController = require('./controller/video.js') -const searchController = require('./controller/search.js') -const websocketController = require('./controller/websocket.js') -const transparencyController = require('./controller/transparency.js') - -const app = express() - -require('express-ws')(app) -app.use(cors()) - -app.get('/latest', latestController.getLatest) -app.get('/sitemap-index.xml', latestController.getSitemap) -app.get('/sitemap-:index.xml', latestController.getSubSitemap) - -app.get('/video/:id', videoController.getVideo) -app.get('/channel/:id', videoController.getChannel) -app.get('/channel/:id/videos', videoController.getOnlyChannelVideos) -app.get('/playlist/:id', videoController.getPlaylist) - -app.get('/search/video', searchController.searchVideo) -app.get('/search/playlist', searchController.searchPlaylist) -app.get('/search/channel', searchController.searchChannel) - -app.get('/transparency/list', transparencyController.getReports) -app.get('/transparency/:id', transparencyController.getReports) - -app.ws('/save', websocketController.save) -app.ws('/saveplaylist', websocketController.playlist) -app.ws('/savechannel', websocketController.channel) -app.get('/autodownload', websocketController.addAutodownload) - -process.on('uncaughtException', err => { - logger.error(err) - console.log(err) -}) - -app.listen(1337, () => { - logger.info({ message: 'Server listening on port 1337!' }) -}) \ No newline at end of file diff --git a/package.json b/package.json index 61b280e..ba3ad49 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,27 @@ { - "name": "preservetube", - "version": "1.0.0", - "main": "index.js", - "license": "AGPL-3.0", - "dependencies": { - "@logtail/node": "^0.4.0", - "@logtail/winston": "^0.4.1", - "@prisma/client": "4.9.0", - "aws-sdk": "2.1128.0", - "cors": "^2.8.5", - "dotenv": "^16.0.3", - "express": "^4.18.2", - "express-ws": "^5.0.2", - "ioredis": "^5.3.1", - "isomorphic-dompurify": "^1.0.0", - "node-fetch": "2", - "readable-to-ms": "^1.0.3", - "rolling-rate-limiter": "^0.4.2", - "winston": "^3.8.2", - "ws": "^8.17.1" + "name": "preservetube-backend", + "module": "src/index.ts", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "bun run --watch src/index.ts" }, "devDependencies": { - "prisma": "4.9.0" + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@types/pg": "^8.11.10", + "date-fns": "^4.1.0", + "elysia": "^1.1.25", + "ioredis": "^5.4.1", + "isomorphic-dompurify": "^2.18.0", + "kysely": "^0.27.4", + "pg": "^8.13.1", + "readable-to-ms": "^1.0.3", + "rolling-rate-limiter": "^0.4.2", + "ultralight-s3": "^0.0.7" } -} +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 3276758..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,44 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model videos { - uuid String @id @default(uuid()) - id String @unique - title String - description String - thumbnail String - source String - published String - archived String - channel String - channelId String - channelVerified Boolean - channelAvatar String - playlist String? - disabled Boolean @default(false) - hasBeenReported Boolean @default(false) - - @@index([title], name: "idx_title") -} - -model reports { - uuid String @id @default(uuid()) - target String - title String - details String - date DateTime @default(now()) -} - -model autodownload { - uuid String @id @default(uuid()) - channel String -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..dc96d78 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,23 @@ +import { Elysia } from 'elysia'; + +import latest from '@/router/latest' +import search from '@/router/search' +import transparency from '@/router/transparency' +import video from '@/router/video' +import websocket from '@/router/websocket' + +const app = new Elysia() +app.use(latest) +app.use(search) +app.use(transparency) +app.use(video) +app.use(websocket) + +process.on('uncaughtException', err => { + console.log(err) +}) + +app.listen(1337); +console.log( + `api is running at ${app.server?.hostname}:${app.server?.port}` +); \ No newline at end of file diff --git a/src/router/latest.ts b/src/router/latest.ts new file mode 100644 index 0000000..d1ba98a --- /dev/null +++ b/src/router/latest.ts @@ -0,0 +1,66 @@ +import { Elysia } from 'elysia'; +import { Redis } from 'ioredis' + +import { db } from '@/utils/database' +import { createSitemapXML, createSitemapIndexXML } from '@/utils/sitemap' + +const app = new Elysia() +const redis = new Redis({ + host: process.env.REDIS_HOST, + password: process.env.REDIS_PASS, +}); + +app.get('/latest', async () => { + const cached = await redis.get('latest') + if (cached) return JSON.parse(cached) + + const json = await db.selectFrom('videos') + .select(['id', 'title', 'thumbnail', 'published', 'archived', 'channel', 'channelId', 'channelAvatar', 'channelVerified']) + .orderBy('archived desc') + .limit(50) + .execute() + + await redis.set('latest', JSON.stringify(json), 'EX', 3600) + + return json +}) + +app.get('/sitemap-index.xml', async ({ set }) => { + const cachedSitemapIndex = await redis.get('sitemap-index'); + if (cachedSitemapIndex) { + set.headers['Content-Type'] = 'application/xml' + return cachedSitemapIndex + } + + const videos = await db.selectFrom('videos') + .select('id') + .execute() + + const urls = videos.map((video) => `https://preservetube.com/watch?v=${video.id}`); + const sitemaps = []; + + for (let i = 0; i < urls.length; i += 50000) { + const batch = urls.slice(i, i + 50000); + await redis.set(`sitemap-${sitemaps.length}`, createSitemapXML(batch), 'EX', 86400); + sitemaps.push(`sitemap-${sitemaps.length}.xml`); + } + + const sitemapIndexXML = createSitemapIndexXML(sitemaps); + await redis.set('sitemap-index', sitemapIndexXML, 'EX', 86400); + + set.headers['Content-Type'] = 'application/xml' + return sitemapIndexXML +}) + +app.get('/sitemap-:index.xml', async ({ set, params: { index }, error, path }) => { + const indexNum = path.replace('/sitemap-', '').replace('.xml', '') + const cachedSitemap = await redis.get(`sitemap-${indexNum}`); + if (cachedSitemap) { + set.headers['Content-Type'] = 'application/xml' + return cachedSitemap + } + + return error(404) +}) + +export default app \ No newline at end of file diff --git a/src/router/search.ts b/src/router/search.ts new file mode 100644 index 0000000..f3f954a --- /dev/null +++ b/src/router/search.ts @@ -0,0 +1,52 @@ +import { Elysia, t } from 'elysia'; +import { Redis } from 'ioredis' +import { RedisRateLimiter } from 'rolling-rate-limiter' + +import { db } from '@/utils/database' +import { validateVideo, validatePlaylist, validateChannel } from '@/utils/regex' + +const app = new Elysia() +const redis = new Redis({ + host: process.env.REDIS_HOST, + password: process.env.REDIS_PASS, +}); + +const limiter = new RedisRateLimiter({ + client: redis, + namespace: 'search:', + interval: 5 * 60 * 1000, + maxInInterval: 15 +}) + +app.get('/search/video', async ({ headers, query: { search }, error }) => { + const hash = Bun.hash(headers['x-userip'] || headers['cf-connecting-ip'] || '0.0.0.0') + const isLimited = await limiter.limit(hash.toString()) + if (isLimited) return error(429, 'error-You have been ratelimited.') + + const videoId = validateVideo(search) + if (videoId) return `redirect-${process.env.FRONTEND}/watch?v=${videoId}` + + const videos = await db.selectFrom('videos') + .selectAll() + .where('title', 'ilike', `%${search}%`) + .execute() + + return videos +}, { + query: t.Object({ + search: t.String() + }) +}) + +app.get('/search/channel', async ({ query: { url }, error, redirect }) => { + const channelId = await validateChannel(url) + if (!channelId) return error(400, 'Whoops! What is that? That is not a Youtube Channel.') + + return redirect(`${process.env.FRONTEND}/channel/${channelId}`) +}, { + query: t.Object({ + url: t.String() + }) +}) + +export default app \ No newline at end of file diff --git a/src/router/transparency.ts b/src/router/transparency.ts new file mode 100644 index 0000000..0deb6c4 --- /dev/null +++ b/src/router/transparency.ts @@ -0,0 +1,45 @@ +import { Elysia } from 'elysia'; +import { Redis } from 'ioredis' + +import { db } from '@/utils/database' + +const app = new Elysia() +const redis = new Redis({ + host: process.env.REDIS_HOST, + password: process.env.REDIS_PASS, +}); + +app.get('/transparency/list', async () => { + const cached = await redis.get('transparency') + if (cached) return JSON.parse(cached) + + const reports = await db.selectFrom('reports') + .selectAll() + .execute() + + const json = reports.map(r => { + return { + ...r, + details: (r.details).split('<').join('<').split('>').join('>'), + date: (r.date).toISOString().slice(0, 10) + } + }) + + await redis.set('transparency', JSON.stringify(json), 'EX', 3600) + return json +}) + +app.get('/transparency/:id', async ({ params: { id } }) => { + const cached = await redis.get(`transparency:${id}`) + if (cached) return JSON.parse(cached) + + const json = await db.selectFrom('reports') + .selectAll() + .where('target', '=', id) + .execute() + + await redis.set(`transparency:${id}`, JSON.stringify(json), 'EX', 3600) + return json +}) + +export default app \ No newline at end of file diff --git a/src/router/video.ts b/src/router/video.ts new file mode 100644 index 0000000..be277b5 --- /dev/null +++ b/src/router/video.ts @@ -0,0 +1,103 @@ +import { Elysia } from 'elysia'; +import { Redis } from 'ioredis' +import DOMPurify from 'isomorphic-dompurify' + +import { db } from '@/utils/database' +import { getChannel, getChannelVideos } from '@/utils/metadata'; +import { convertRelativeToDate } from '@/utils/common'; + +const app = new Elysia() +const redis = new Redis({ + host: process.env.REDIS_HOST, + password: process.env.REDIS_PASS, +}); + +interface processedVideo { + id: string; + title: string; + thumbnail: string; + published: string; + deleted?: undefined; +} + +app.get('/video/:id', async ({ params: { id }, error }) => { + const cached = await redis.get(`video:${id}`) + if (cached) return JSON.parse(cached) + + const json = await db.selectFrom('videos') + .selectAll() + .where('id', '=', id) + .executeTakeFirst() + + if (!json) return error(404, { error: '404' }) + await redis.set(`video:${id}`, JSON.stringify(json), 'EX', 3600) + + return { + ...json, + description: DOMPurify.sanitize(json.description), + } +}) + +app.get('/channel/:id', async ({ params: { id }, error }) => { + const cached = await redis.get(`channel:${id}`) + if (cached) return JSON.parse(cached) + + const [videos, channel] = await Promise.all([ + getChannelVideos(id), + getChannel(id) + ]) + + if (!videos || !channel || videos.error || channel.error) return error(404, { error: '404' }) + + const archived = await db.selectFrom('videos') + .select(['id', 'title', 'thumbnail', 'published', 'archived']) + .where('channelId', '=', id) + .execute() + + const processedVideos: processedVideo[] = videos.map((video: any) => ({ // it would be impossible to set types for youtube output... they change it every day. + id: video.id, + title: video.title.text, + thumbnail: video.thumbnails[0].url, + published: (video.published.text.endsWith('ago') ? convertRelativeToDate(video.published.text) : new Date(video.published.text)).toISOString().slice(0, 10) + })) + + archived.forEach(v => { + const existingVideoIndex = processedVideos.findIndex(video => video.id === v.id); + if (existingVideoIndex !== -1) { + processedVideos[existingVideoIndex] = v; + } else { + processedVideos.push({ ...v, deleted: undefined }); + } + }); + + processedVideos.sort((a: any, b: any) => new Date(b.published).getTime() - new Date(a.published).getTime()); + + const json = { + name: channel.metadata.title, + avatar: channel.metadata.avatar[0].url, + verified: channel.header.author?.is_verified, + videos: processedVideos + } + + await redis.set(`channel:${id}`, JSON.stringify(json), 'EX', 3600) + return json +}) + +app.get('/channel/:id/videos', async ({ params: { id } }) => { + const cached = await redis.get(`channelVideos:${id}`) + if (cached) return JSON.parse(cached) + + const archived = await db.selectFrom('videos') + .select(['id', 'title', 'thumbnail', 'published', 'archived']) + .where('channelId', '=', id) + .orderBy('published desc') + .execute() + + const json = { + videos: archived + } + await redis.set(`channelVideos:${id}`, JSON.stringify(json), 'EX', 3600) + return json +}) + +export default app \ No newline at end of file diff --git a/src/router/websocket.ts b/src/router/websocket.ts new file mode 100644 index 0000000..ce502f6 --- /dev/null +++ b/src/router/websocket.ts @@ -0,0 +1,179 @@ +import { Elysia, t } from 'elysia'; +import { Redis } from 'ioredis' +import * as fs from 'node:fs' + +import { db } from '@/utils/database' +import { validateVideo, validateChannel } from '@/utils/regex' +import { checkCaptcha, createDatabaseVideo } from '@/utils/common'; +import { downloadVideo } from '@/utils/download'; +import { uploadVideo } from '@/utils/upload'; +import { getChannelVideos } from '@/utils/metadata'; + +const app = new Elysia() +const redis = new Redis({ + host: process.env.REDIS_HOST, + password: process.env.REDIS_PASS, +}); +const videoIds: Record = {} + +const sendError = (ws: any, message: string) => { + ws.send(`ERROR - ${message}`); + ws.close(); +}; + +const cleanup = async (ws: any, videoId: string) => { + delete videoIds[ws.id]; + if (videoId) await redis.del(videoId); + await redis.del(ws.id); +}; + +const handleUpload = async (ws: any, videoId: string, isChannel: boolean = false) => { + const filePath = `./videos/${videoId}.mp4`; + if (!fs.existsSync(filePath)) { + ws.send(`DATA - Video file for ${videoId} not found. Skipping.`); + return false; + } + + try { + ws.send('DATA - Uploading file...'); + const videoUrl = await uploadVideo(filePath); + fs.unlinkSync(filePath); + + const uploaded = await createDatabaseVideo(videoId, videoUrl); + if (uploaded !== 'success') { + ws.send(`DATA - Error while uploading - ${JSON.stringify(uploaded)}`); + return false; + } + + if (!isChannel) ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`); + return true; + } catch (error: any) { + ws.send(`ERROR - Upload failed for ${videoId}: ${error.message}`); + fs.unlinkSync(filePath); + return false; + } +}; + + +app.ws('/save', { + query: t.Object({ + url: t.String() + }), + body: t.String(), + open: async (ws) => { + console.log(`${ws.id} - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`) + + const videoId = validateVideo(ws.data.query.url) + if (!videoId) return sendError(ws, 'Invalid video URL.'); + if (await redis.get(videoId)) return sendError(ws, 'Someone is already downloading this video...'); + if (await redis.get(`blacklist:${videoId}`)) return sendError(ws, 'This video is blacklisted.'); + + const already = await db.selectFrom('videos') + .select('id') + .where('id', '=', videoId) + .executeTakeFirst() + + if (already) { + ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`) + ws.close() + } else { + ws.send('DATA - This process is automatic. Your video will start archiving shortly.') + ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') + videoIds[ws.id] = videoId + } + }, + message: async (ws, message) => { + if (message == 'alive') return + + const videoId = videoIds[ws.id]; + if (!videoId) return sendError(ws, 'No video ID associated with this session.'); + + if (await redis.get(videoId) !== 'downloading') { + await redis.set(videoId, 'downloading', 'EX', 300) + + if (!(await checkCaptcha(message))) { + await cleanup(ws, videoId); + return sendError(ws, 'Captcha validation failed.'); + } + + const downloadResult = await downloadVideo(ws, videoId); + if (downloadResult.fail) { + await cleanup(ws, videoId); + return sendError(ws, downloadResult.message); + } + + const uploadSuccess = await handleUpload(ws, videoId); + if (!uploadSuccess) await redis.del(videoId); + + await cleanup(ws, videoId); + ws.close(); + } else { + ws.send('DATA - Captcha already submitted.'); + } + }, + close: async (ws) => { + await cleanup(ws, videoIds[ws.id]); + console.log(`closed - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`) + } +}) + +app.ws('/savechannel', { + query: t.Object({ + url: t.String() + }), + body: t.String(), + open: async (ws) => { + console.log(`${ws.id} - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`) + + const channelId = await validateChannel(ws.data.query.url); + if (!channelId) return sendError(ws, 'Invalid channel URL.'); + + ws.send('DATA - This process is automatic. Your video will start archiving shortly.') + ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') + videoIds[ws.id] = `captcha-${channelId}`; + }, + message: async (ws, message) => { + if (message == 'alive') return + + const status = videoIds[ws.id]; + if (!status || !status.startsWith('captcha-')) return sendError(ws, 'No channel associated with this session.'); + + const channelId = status.replace('captcha-', ''); + if (!(await checkCaptcha(message))) { + await cleanup(ws, channelId); + return sendError(ws, 'Captcha validation failed.'); + } + + videoIds[ws.id] = `downloading-${channelId}`; + const videos = await getChannelVideos(channelId); + + for (const video of videos.slice(0, 5)) { + if (!video || (await redis.get(video.id)) || (await redis.get(`blacklist:${video.id}`))) continue; + + const already = await db.selectFrom('videos') + .select('id') + .where('id', '=', video.id) + .executeTakeFirst() + if (already) continue + + ws.send(`DATA - Processing video: ${video.title.text}`); + await redis.set(video.id, 'downloading', 'EX', 300); + + const downloadResult = await downloadVideo(ws, video.id); + if (!downloadResult.fail) await handleUpload(ws, video.id, true); + + await redis.del(video.id); + ws.send(`DATA - Created video page for ${video.title.text}`) + } + + await cleanup(ws, channelId); + ws.send(`DONE - ${process.env.FRONTEND}/channel/${channelId}`) + ws.close(); + }, + close: async (ws) => { + await cleanup(ws, videoIds[ws.id]); + console.log(`closed - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`) + } +}) + +export default app \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..580ae0a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,45 @@ +import type { + Generated, + Insertable, + Selectable, + Updateable, +} from 'kysely' + +export interface Database { + videos: VideosTable + reports: ReportsTable +} + +export interface VideosTable { + uuid: Generated + id: string + title: string + description: string + thumbnail: string + source: string + published: string + archived: string + channel: string + channelId: string + channelVerified: boolean + channelAvatar: string + playlist?: string | null + disabled: boolean + hasBeenReported: boolean +} + +export type Video = Selectable +export type NewVideo = Insertable +export type UpdateVideo = Updateable + +export interface ReportsTable { + uuid: Generated + target: string + title: string + details: string + date: Date +} + +export type Report = Selectable +export type NewReport = Insertable +export type UpdateReport = Updateable \ No newline at end of file diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 0000000..5a0a963 --- /dev/null +++ b/src/utils/common.ts @@ -0,0 +1,86 @@ +import { getVideo, getChannel } from "@/utils/metadata"; +import { uploadImage } from "@/utils/upload"; +import { db } from '@/utils/database' +import crypto from 'node:crypto'; + +function convertRelativeToDate(relativeTime: string) { + const parts = relativeTime.split(' '); + const amount = parseInt(parts[0]); + const unit = parts[1]; + + const currentDate = new Date(); + + switch (unit) { + case 'hour': + case 'hours': + currentDate.setHours(currentDate.getHours() - amount); + break; + case 'minute': + case 'minutes': + currentDate.setMinutes(currentDate.getMinutes() - amount); + break; + case 'day': + case 'days': + currentDate.setDate(currentDate.getDate() - amount); + break; + case 'month': + case 'months': + currentDate.setMonth(currentDate.getMonth() - amount); + break; + case 'year': + case 'years': + currentDate.setFullYear(currentDate.getFullYear() - amount); + break; + } + + return currentDate; +} + +async function checkCaptcha(response: string) { + const confirm = await (await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + body: JSON.stringify({ + 'response': response, + 'secret': process.env.CAPTCHA_SECRET + }), + headers: { + 'content-type': 'application/json' + } + })).json() + + return confirm.success +} + +async function createDatabaseVideo(id: string, videoUrl: string) { + const data = await getVideo(id) + const channelData = await getChannel(data.basic_info.channel_id) + + if (data.error) return data + if (channelData.error) return channelData + + const uploaderAvatar = await uploadImage(data.basic_info.channel_id, channelData.metadata.thumbnail[0].url) + const thumbnailUrl = await uploadImage(id, data.basic_info.thumbnail[0].url) + + await db.insertInto('videos') + .values({ + uuid: crypto.randomUUID(), + id: id, + title: data.basic_info.title, + description: (data.basic_info.short_description).replaceAll('\n', '
'), + thumbnail: thumbnailUrl, + source: videoUrl, + published: (data.primary_info.published.text.endsWith('ago') ? convertRelativeToDate(data.primary_info.published.text) : new Date(data.primary_info.published.text)).toISOString().slice(0, 10), + archived: (new Date()).toISOString().slice(0, 10), + channel: channelData.metadata.title, + channelId: channelData.metadata.external_id, + channelVerified: channelData.header.author?.is_verified || false, + channelAvatar: uploaderAvatar, + disabled: false, + hasBeenReported: false + }) + .execute() + + return 'success' +} + +export { convertRelativeToDate, checkCaptcha, createDatabaseVideo } \ No newline at end of file diff --git a/src/utils/database.ts b/src/utils/database.ts new file mode 100644 index 0000000..30de953 --- /dev/null +++ b/src/utils/database.ts @@ -0,0 +1,14 @@ +import type { Database } from '@/types.ts' +import { Pool } from 'pg' +import { Kysely, PostgresDialect } from 'kysely' + +const dialect = new PostgresDialect({ + pool: new Pool({ + connectionString: process.env.DATABASE_URL, + max: 10, + }) +}) + +export const db = new Kysely({ + dialect, +}) \ No newline at end of file diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 0000000..b04fe4d --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1,62 @@ +import WebSocket from 'ws'; +import { getVideo } from '@/utils/metadata'; + +async function downloadVideo(ws: any, id: string): Promise<{ fail: boolean, message: string }> { + return new Promise(async (resolve, reject) => { + let quality = '480p' + const video = await getVideo(id) + if (video.error) { + return resolve({ + message: `Failed to request Youtube with error ${video.error}. Please retry...`, + fail: true + }) + } + if (video.basic_info.duration >= 900) quality = '360p' // 15 minutes + + quality = await getVideoQuality(video, quality) + + let isDownloading = true + const downloader = new WebSocket(`ws://${(process.env.METADATA!).replace('http://', '')}/download/${id}/${quality}`) + + downloader.on('message', async function message(data: any) { + const text = data.toString() + if (text == 'done') { + isDownloading = false + return resolve({ + fail: false, + message: '' + }) + } else { + ws.send(`DATA - ${text}`) + } + }) + + downloader.on('close', function close() { + if (!isDownloading) return + + return resolve({ + fail: true, + message: 'The metadata server unexpectedly closed the websocket. Please try again.' + }) + }) + }) +} + +async function getVideoQuality(json: any, quality: string) { + const adaptiveFormats = json['streaming_data']['adaptive_formats']; + let video = adaptiveFormats.find((f: any) => f.quality_label === quality && !f.has_audio); + + // If the specified quality isn't available, find the lowest quality video + if (!video) { // @ts-ignore + video = adaptiveFormats.filter((f: any) => !f.has_audio).reduce((prev, current) => { + if (!prev || parseInt(current.quality_label) < parseInt(prev.quality_label)) { + return current; + } + return prev; + }, null); + } + + return video ? video.quality_label : null; +} + +export { downloadVideo } \ No newline at end of file diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts new file mode 100644 index 0000000..45c7e2c --- /dev/null +++ b/src/utils/metadata.ts @@ -0,0 +1,15 @@ +// metadata either returns innertube or { error: string } + +async function getVideo(id: string) { + return await (await fetch(`${process.env.METADATA}/video/${id}`)).json() +} + +async function getChannel(id: string) { + return await (await fetch(`${process.env.METADATA}/channel/${id}`)).json() +} + +async function getChannelVideos(id: string) { + return await (await fetch(`${process.env.METADATA}/videos/${id}`)).json() +} + +export { getVideo, getChannel, getChannelVideos } \ No newline at end of file diff --git a/src/utils/regex.ts b/src/utils/regex.ts new file mode 100644 index 0000000..4c78b9f --- /dev/null +++ b/src/utils/regex.ts @@ -0,0 +1,92 @@ +function validateVideo(input: string): string | false { + try { + const url = new URL(input); + const hostnames = [ + 'youtube.com', + 'www.youtube.com', + 'm.youtube.com' + ]; + + // basic hostname check + if (hostnames.includes(url.hostname)) { + // basic url + if (url.pathname === '/watch') { + const videoId = url.searchParams.get('v'); + return videoId || false; + } + + // embed url + const embedMatch = url.pathname.match(/^\/embed\/([a-zA-Z0-9_-]+)/); + if (embedMatch) { + return embedMatch[1]; + } + + return false; + } + + // short urls + if (url.hostname === 'youtu.be') { + const videoId = url.pathname.replace(/^\//, ''); + return videoId || false; + } + + return false; + } catch { + return false; + } +} + +function validatePlaylist(input: string): string | false { + try { + const url = new URL(input); + const hostnames = [ + 'youtube.com', + 'www.youtube.com', + 'm.youtube.com' + ]; + + if (hostnames.includes(url.hostname)) { + // all urls are the same, thank god + if (url.pathname === '/playlist' || url.pathname === '/watch') { + const playlistId = url.searchParams.get('list'); + return playlistId || false; + } + } + + return false; + } catch { + return false; + } +} + +async function validateChannel(input: string): Promise { + try { + const url = new URL(input); + const hostnames = [ + 'youtube.com', + 'www.youtube.com', + 'm.youtube.com' + ]; + + if (hostnames.includes(url.hostname)) { + // @ urls + const atMatch = url.pathname.match(/^\/@([a-zA-Z0-9.-]+)/); + if (atMatch) { + const channelId = await (await fetch(`https://yt.jaybee.digital/api/channels?part=channels&handle=${atMatch[1]}`)).json() + return channelId['items'][0]['id'] + } + + // /channel/ and /c/ + const channelMatch = url.pathname.match(/^\/(channel|c)\/([a-zA-Z0-9_-]+)/); + if (channelMatch) { + return channelMatch[2]; + } + } + + return false; + } catch { + return false; + } +} + +export { validateVideo, validatePlaylist, validateChannel } \ No newline at end of file diff --git a/src/utils/sitemap.ts b/src/utils/sitemap.ts new file mode 100644 index 0000000..16851e8 --- /dev/null +++ b/src/utils/sitemap.ts @@ -0,0 +1,32 @@ +import { format } from 'date-fns'; + +function createSitemapXML(urls: string[]) { + const xml = urls.map(url => ` + + ${encodeURI(url)} + never + 0.7 + + `).join(''); + + return ` + + ${xml} +`; +} + +function createSitemapIndexXML(sitemaps: string[]) { + const xml = sitemaps.map(sitemap => ` + + ${encodeURI(sitemap)} + ${format(new Date(), 'yyyy-MM-dd')} + + `).join(''); + + return ` + + ${xml} +`; +} + +export { createSitemapXML, createSitemapIndexXML } \ No newline at end of file diff --git a/src/utils/upload.ts b/src/utils/upload.ts new file mode 100644 index 0000000..5f27395 --- /dev/null +++ b/src/utils/upload.ts @@ -0,0 +1,38 @@ +import { S3 } from 'ultralight-s3'; +import * as fs from 'node:fs' + +const keys = JSON.parse(fs.readFileSync('s3.json', 'utf-8')) +const videos3 = new S3({ + endpoint: keys.endpoint, + accessKeyId: keys.videos[0].access, + secretAccessKey: keys.videos[0].secret, + bucketName: keys.videos[0].bucket, + region: 'auto' +}) + +const images3 = new S3({ + endpoint: keys.endpoint, + accessKeyId: keys.images[0].access, + secretAccessKey: keys.images[0].secret, + bucketName: keys.images[0].bucket, + region: 'auto' +}); + +async function uploadVideo(video: string) { + const videoFile = fs.readFileSync(video) + const uploaded = await videos3.put(video.split('/')[2], videoFile) + return uploaded.url.replace(keys.endpoint, 'https://s2.archive.party') +} + +async function uploadImage(id: string, url: string) { + const exists = await images3.fileExists(`${id}.webp`) + if (exists) return `${keys.images[0].url}${id}.webp` + + const response = await fetch(url) + const buffer = Buffer.from(await response.arrayBuffer()) + + const uploaded = await images3.put(`${id}.webp`, buffer) + return uploaded.url.replace(keys.endpoint, 'https://s2.archive.party') +} + +export { uploadVideo, uploadImage } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c4f033 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/utils/captcha.js b/utils/captcha.js deleted file mode 100644 index f5c4498..0000000 --- a/utils/captcha.js +++ /dev/null @@ -1,18 +0,0 @@ -const fetch = require('node-fetch') - -async function checkCaptcha(response) { - const confirm = await (await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { - method: 'POST', - body: JSON.stringify({ - 'response': response, - 'secret': process.env.CAPTCHA_SECRET - }), - headers: { - 'content-type': 'application/json' - } - })).json() - - return confirm.success -} - -module.exports = { checkCaptcha } \ No newline at end of file diff --git a/utils/logger.js b/utils/logger.js deleted file mode 100644 index d1e5eb8..0000000 --- a/utils/logger.js +++ /dev/null @@ -1,27 +0,0 @@ -const winston = require('winston') -const { Logtail } = require("@logtail/node") -const { LogtailTransport } = require("@logtail/winston") - -const logtail = new Logtail(process.env.LOGTAIL) - -const logger = winston.createLogger({ - format: winston.format.json(), - transports: [ - new winston.transports.Console({ - format:winston.format.combine( - winston.format.timestamp({format: 'MMM-DD-YYYY HH:mm:ss'}), - winston.format.printf(info => `${[info.timestamp]}: ${info.message}`), - )}), - new winston.transports.File({ - filename: 'logs/client.log', - format:winston.format.combine( - winston.format.timestamp({format: 'MMM-DD-YYYY HH:mm:ss'}), - winston.format.printf(info => `${[info.timestamp]}: ${info.message}`), - )}), - new LogtailTransport(logtail, { - level: 'error' - }) - ], -}); - -module.exports = logger \ No newline at end of file diff --git a/utils/metadata.js b/utils/metadata.js deleted file mode 100644 index cccd6ab..0000000 --- a/utils/metadata.js +++ /dev/null @@ -1,57 +0,0 @@ -const fetch = require('node-fetch') - -async function getPipedInstance() { - const instances = await (await fetch('https://piped-instances.kavin.rocks/', { - headers: { - 'User-Agent': 'Mozilla/5.0 (compatible; PreserveTube/0.0; +https://preservetube.com)' - } - })).json() - return (instances[Math.floor(Math.random() * instances.length)]).api_url -} - -async function getVideoMetadata(id) { - return await (await fetch(`${process.env.METADATA}/video/${id}`)).json() -} - -async function getChannel(id) { - return await (await fetch(`${process.env.METADATA}/channel/${id}`)).json() -} - -async function getChannelVideos(id) { - return await (await fetch(`${process.env.METADATA}/videos/${id}`)).json() -} - -async function getPlaylistVideos(id) { - const instance = await getPipedInstance() - const json = await (await fetch(`${instance}/playlists/${id}`, { - headers: { - 'User-Agent': 'Mozilla/5.0 (compatible; PreserveTube/0.0; +https://preservetube.com)' - } - })).json() - return json -} - -async function getVideoDownload(json, quality) { - const adaptiveFormats = json['streaming_data']['adaptive_formats']; - let video = adaptiveFormats.find(f => f.quality_label === `${quality}p` && !f.has_audio); - if (!video) { // stupid bullshit. basically finding the lowest quality if the quality specified isnt there - video = adaptiveFormats.filter(f => !f.has_audio).reduce((prev, current) => { - if (!prev || parseInt(current.quality_label) < parseInt(prev.quality_label)) { - return current; - } - return prev; - }, null); - } - - const audio = adaptiveFormats.find(f => f.has_audio); - - return { - url: [ - video.url, - audio.url - ] - }; -} - - -module.exports = { getVideoMetadata, getChannel, getChannelVideos, getPlaylistVideos, getVideoDownload } \ No newline at end of file diff --git a/utils/redis.js b/utils/redis.js deleted file mode 100644 index 14694fe..0000000 --- a/utils/redis.js +++ /dev/null @@ -1,33 +0,0 @@ -const Redis = require('ioredis') -const fs = require('node:fs') - -const logger = require("./logger.js") - -const redis = new Redis({ - host: process.env.REDIS_HOST, - port: process.env.REDIS_PORT, - password: process.env.REDIS_PASS, -}); - -redis.on('ready', async function () { - logger.info({ message: 'Connected to redis!' }) - - const keys = await redis.keys('*') - const filteredKeys = keys.filter(key => !key.startsWith('blacklist:')) - if (filteredKeys.length) await redis.del(filteredKeys) - - setInterval(async () => { - const files = fs.readdirSync('videos') - const webmFiles = files.filter((file) => file.endsWith('.mp4')) - webmFiles.forEach(async (f) => { - const videoId = f.replace('.mp4', '') - const isActive = await redis.get(videoId) - if (!isActive) { - fs.unlinkSync(`./videos/${f}`) - logger.info({ message: `deleted file ${f} because there is no active download of it` }) - } - }) - }, 5*60000) -}) - -module.exports = redis \ No newline at end of file diff --git a/utils/upload.js b/utils/upload.js deleted file mode 100644 index a2fa76d..0000000 --- a/utils/upload.js +++ /dev/null @@ -1,68 +0,0 @@ -const AWS = require('aws-sdk') -const fs = require('node:fs') - -const keys = require('../s3.json') - -async function uploadVideo(video) { - const key = keys.videos[0] - - const s3 = new AWS.S3({ - accessKeyId: key.access, - secretAccessKey: key.secret, - endpoint: keys.endpoint, - s3ForcePathStyle: true - }) - - const videoFile = fs.createReadStream(video) - const uploaded = await s3.upload({ - Bucket: key.bucket, - Key: video.split('/')[2], - Body: videoFile, - ContentType: 'video/mp4', - }).promise() - - return (uploaded.Location).replace(keys.endpoint, 'https://s2.archive.party') -} - -async function uploadImage(id, url) { - const key = keys.images[0] - - const s3 = new AWS.S3({ - accessKeyId: key.access, - secretAccessKey: key.secret, - endpoint: keys.endpoint, - s3ForcePathStyle: true - }) - - const exists = await checkIfFileExists({ - Bucket: key.bucket, - Key: `${id}.webp` - }, s3) - - if (exists) { - return `${key.url}${id}.webp` - } else { - const response = await fetch(url) - const buffer = Buffer.from(await response.arrayBuffer()) - - const uploaded = await s3.upload({ - Bucket: key.bucket, - Key: `${id}.webp`, - Body: buffer, - ContentType: 'video/webp', - }).promise() - - return (uploaded.Location).replace(keys.endpoint, 'https://s2.archive.party') - } -} - -async function checkIfFileExists(params, s3) { - try { - await s3.headObject(params).promise() - return true - } catch (err) { - return false - } -} - -module.exports = { uploadVideo, uploadImage } \ No newline at end of file diff --git a/utils/validate.js b/utils/validate.js deleted file mode 100644 index f8edd8e..0000000 --- a/utils/validate.js +++ /dev/null @@ -1,53 +0,0 @@ -const fetch = require('node-fetch') - -async function validateVideoInput(input) { - if (!input) return { - fail: true, - message: 'Missing URL' - } - - const id = (input.trim()).match(/^https:\/\/(?:(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([A-Za-z0-9_-]{11})$/m)?.[1] - if (!id) return { - fail: true, - message: 'Whoops! What is that? That is not a Youtube url.' - } - - return id -} - -async function validatePlaylistInput(input) { - if (!input) return { - fail: true, - message: 'Missing URL' - } - - const id = (input.trim()).match(/^(?:https?:\/\/)?(?:www\.)?youtu(?:(?:\.be)|(?:be\.com))\/playlist\?list=([\w_-]{34})$/m)?.[1] - if (!id) return { - fail: true, - message: 'Whoops! What is that? That is not a Youtube Playlist.' - } - - return id -} - -async function validateChannelInput(input) { - if (!input) return { - fail: true, - message: 'Missing URL' - } - - const id = input.match(/^(?:https?:\/\/)?(?:www\.)?youtu(?:(?:\.be)|(?:be\.com))\/(?:channel\/|@)([\w-]+)/m)?.[1] - if (!id) return { - fail: true, - message: 'Whoops! What is that? That is not a Youtube Channel.' - } - - if (input.includes('@')) { - const channelId = await (await fetch(`https://yt.jaybee.digital/api/channels?part=channels&handle=${id}`)).json() - return channelId['items'][0]['id'] - } else { - return id - } -} - -module.exports = { validateVideoInput, validatePlaylistInput, validateChannelInput } \ No newline at end of file diff --git a/utils/websocket.js b/utils/websocket.js deleted file mode 100644 index 81a4728..0000000 --- a/utils/websocket.js +++ /dev/null @@ -1,62 +0,0 @@ -const { PrismaClient } = require('@prisma/client') -const prisma = new PrismaClient() - -const metadata = require('./metadata.js') -const upload = require('./upload.js') - -function convertRelativeToDate(relativeTime) { - const parts = relativeTime.split(' '); - const amount = parseInt(parts[0]); - const unit = parts[1]; - - const currentDate = new Date(); - - switch (unit) { - case 'hour': - case 'hours': - currentDate.setHours(currentDate.getHours() - amount); - break; - case 'minute': - case 'minutes': - currentDate.setMinutes(currentDate.getMinutes() - amount); - break; - case 'day': - case 'days': - currentDate.setDate(currentDate.getDate() - amount); - break; - } - - return currentDate; -} - -async function createDatabaseVideo(id, videoUrl, playlistId) { - const data = await metadata.getVideoMetadata(id) - const channelData = await metadata.getChannel(data.basic_info.channel_id) - - if (data.error) return data - if (channelData.error) return channelData - - const uploaderAvatar = await upload.uploadImage(data.basic_info.channel_id, channelData.metadata.thumbnail[0].url) - const thumbnailUrl = await upload.uploadImage(id, data.basic_info.thumbnail[0].url) - - await prisma.videos.create({ - data: { - id: id, - title: data.basic_info.title, - description: (data.basic_info.short_description).replaceAll('\n', '
'), - thumbnail: thumbnailUrl, - source: videoUrl, - published: (data.primary_info.published.text.endsWith('ago') ? convertRelativeToDate(data.primary_info.published.text) : new Date(data.primary_info.published.text)).toISOString().slice(0, 10), - archived: (new Date()).toISOString().slice(0, 10), - channel: channelData.metadata.title, - channelId: channelData.metadata.external_id, - channelAvatar: uploaderAvatar, - channelVerified: channelData.header.author?.is_verified || false, - playlist: playlistId - } - }) - - return 'success' -} - -module.exports = { createDatabaseVideo } \ No newline at end of file diff --git a/utils/ytdlp.js b/utils/ytdlp.js deleted file mode 100644 index a8c6a56..0000000 --- a/utils/ytdlp.js +++ /dev/null @@ -1,60 +0,0 @@ -const WebSocket = require('ws') -const metadata = require('./metadata.js') - -async function downloadVideo(url, ws, id) { - return new Promise(async (resolve, reject) => { - let quality = '480p' - const video = await metadata.getVideoMetadata(id) - if (video.error) { - return resolve({ - message: `Failed to request Youtube with error ${video.error}. Please retry...`, - fail: true - }) - } - if (video.basic_info.duration >= 900) quality = '360p' // 15 minutes - - quality = await getVideoQuality(video, quality) - - let isDownloading = true - const downloader = new WebSocket(`ws://${process.env.METADATA.replace('http://', '')}/download/${id}/${quality}`) - downloader.on('message', async function message(data) { - const text = data.toString() - if (text == 'done') { - isDownloading = false - return resolve({ - fail: false - }) - } else { - ws.send(`DATA - ${text}`) - } - }) - - downloader.on('close', function close(code, reason) { - if (!isDownloading) return - - return resolve({ - fail: true, - message: 'The metadata server unexpectedly closed the websocket. Please try again.' - }) - }) - }) -} - -async function getVideoQuality(json, quality) { - const adaptiveFormats = json['streaming_data']['adaptive_formats']; - let video = adaptiveFormats.find(f => f.quality_label === quality && !f.has_audio); - - // If the specified quality isn't available, find the lowest quality video - if (!video) { - video = adaptiveFormats.filter(f => !f.has_audio).reduce((prev, current) => { - if (!prev || parseInt(current.quality_label) < parseInt(prev.quality_label)) { - return current; - } - return prev; - }, null); - } - - return video ? video.quality_label : null; -} - -module.exports = { downloadVideo } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index c4f08e3..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1392 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== - dependencies: - colorspace "1.1.x" - enabled "2.0.x" - kuler "^2.0.0" - -"@ioredis/commands@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" - integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== - -"@logtail/core@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@logtail/core/-/core-0.4.0.tgz#a8e510756bf9fd1b2cc6c901225eb03685919ca8" - integrity sha512-xItEU5vY2LvcJyAVkxKKZ3UvG3CsK2pMvhuLDntlC+US1LlNcfQmVzvInN5j5iXl0kjNO6n+0EY2YyhhOgjWCQ== - dependencies: - "@logtail/tools" "^0.4.0" - "@logtail/types" "^0.4.0" - -"@logtail/node@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@logtail/node/-/node-0.4.0.tgz#71488911a6dc4bf218871af80e3788cd727f8474" - integrity sha512-0/1to4HLf95FrDlZoFLz3A0Fz/B55G82syY22Yl9N4mjC2wW6t5Lo9d4q/u/VmHBHW3PBbytbmFEoHx0H5Sarg== - dependencies: - "@logtail/core" "^0.4.0" - "@logtail/types" "^0.4.0" - "@msgpack/msgpack" "^2.5.1" - "@types/stack-trace" "^0.0.29" - cross-fetch "^3.0.4" - minimatch "^3.0.4" - stack-trace "^0.0.10" - -"@logtail/tools@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@logtail/tools/-/tools-0.4.0.tgz#ff1bed193a005b12007df510f6ce294b8f518207" - integrity sha512-7lSKdJTq7NMUvriMhkf6cTxY9QlXi+YLLuHxbSOKI56HpJN8qJGPVcmOdzlbk8pJofe9RnME8hBMLTHAD3bmsw== - dependencies: - "@logtail/types" "^0.4.0" - -"@logtail/types@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@logtail/types/-/types-0.4.0.tgz#39c28e08f805657c70107071dc166866153563a8" - integrity sha512-2CR3w7Xf5rKWbUlgiaBKeVrqWuWnVBz0Ymycw/gYwupVI0fb6Ameid9fHfeM5LI/gbIO3ZTIMADlA0FvFbuXMQ== - dependencies: - js "^0.1.0" - -"@logtail/winston@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@logtail/winston/-/winston-0.4.1.tgz#bbdb60db0ae1e013a1e9678ba25160eb2554fecf" - integrity sha512-YsMXzhcpGHDd3ijyqnDD+2qraJAiar/mVjR7HP8NYQsQMk9dQTIHbQzXkBUwAfVOsKSzNbfzeWTnft8sQBYxYQ== - dependencies: - "@logtail/node" "^0.4.0" - "@logtail/types" "^0.4.0" - winston-transport "^4.3.0" - -"@msgpack/msgpack@^2.5.1": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-2.8.0.tgz#4210deb771ee3912964f14a15ddfb5ff877e70b9" - integrity sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ== - -"@prisma/client@4.9.0": - version "4.9.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.9.0.tgz#4a4068f3540732ea5723c008d49ed684d20f9340" - integrity sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg== - dependencies: - "@prisma/engines-version" "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5" - -"@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5": - version "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz#9d817a5779fc05b107eb02f63d197ad296d60b3c" - integrity sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA== - -"@prisma/engines@4.9.0": - version "4.9.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.9.0.tgz#05a1411964e047c1bc43f777c7a1c69f86a2a26c" - integrity sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw== - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@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== - dependencies: - "@types/trusted-types" "*" - -"@types/stack-trace@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/stack-trace/-/stack-trace-0.0.29.tgz#eb7a7c60098edb35630ed900742a5ecb20cfcb4d" - integrity sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g== - -"@types/triple-beam@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8" - integrity sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g== - -"@types/trusted-types@*": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" - integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g== - -abab@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" - integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== - -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== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-globals@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" - integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== - dependencies: - acorn "^8.1.0" - acorn-walk "^8.0.2" - -acorn-walk@^8.0.2: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.1.0, acorn@^8.8.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "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== - -async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -aws-sdk@2.1128.0: - version "2.1128.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1128.0.tgz#1d8762d8124a3ca1d5ee01c3da063fa6cf72381e" - integrity sha512-A3YjMQHDtV4mXf35m8zlM+nsVq5KfyKeiXrEMG7Bdlucuxl7aPdKEmYrPQLw9ZyhYY/x4o08pPuTr/stJaVGeg== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.16.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - 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.1" - type-is "~1.6.18" - unpipe "1.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -buffer@4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -call-bind@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -cluster-key-slot@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" - integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== - -color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.6.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.1.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== - dependencies: - color "^3.1.3" - text-hex "1.0.x" - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-1.1.1.tgz#50d1651868ae60eccff0a2d9f34595376bc6b041" - integrity sha512-71Rod2AhcH3JhkBikVpNd0pA+fWsmAaVoti6OR38T76chA7vE3pSerS0Jor4wDw+tOueD2zLVvFOw5H0Rcj7rA== - dependencies: - keypress "0.1.x" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -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: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -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.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -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" - -cross-fetch@^3.0.4: - version "3.1.8" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" - integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== - dependencies: - node-fetch "^2.6.12" - -cssom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" - integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== - -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssstyle@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" - integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== - dependencies: - cssom "~0.3.6" - -data-urls@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" - integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== - dependencies: - abab "^2.0.6" - whatwg-mimetype "^3.0.0" - whatwg-url "^11.0.0" - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -decimal.js@^10.4.2: - version "10.4.3" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" - integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== - -deep-is@~0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -denque@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" - integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -domexception@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" - integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== - dependencies: - webidl-conversions "^7.0.0" - -dompurify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.0.tgz#6adc6f918376d93419ed1ee35811850680027cba" - integrity sha512-0g/yr2IJn4nTbxwL785YxS7/AvvgGFJw6LLWP+BzWzB1+BYOqPUT9Hy0rXrZh5HLdHnxH72aDdzvC9SdTjsuaA== - -dotenv@^16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" - integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -enabled@2.0.x: - version "2.0.0" - resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" - integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -entities@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== - dependencies: - esprima "^4.0.1" - estraverse "^5.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -esprima@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -events@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== - -express-ws@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/express-ws/-/express-ws-5.0.2.tgz#5b02d41b937d05199c6c266d7cc931c823bda8eb" - integrity sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ== - dependencies: - ws "^7.4.6" - -express@^4.18.2: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.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" - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fecha@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" - integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== - -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" - -fn.name@1.x.x: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" - integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -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" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-intrinsic@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -html-encoding-sniffer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" - integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== - dependencies: - whatwg-encoding "^2.0.0" - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -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: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ieee754@1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -inherits@2.0.4, inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ioredis@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.1.tgz#55d394a51258cee3af9e96c21c863b1a97bf951f" - integrity sha512-C+IBcMysM6v52pTLItYMeV4Hz7uriGtoJdz7SSBDX6u+zwSYGirLdQh3L7t/OItWITcw3gTFMjJReYUwS4zihg== - dependencies: - "@ioredis/commands" "^1.1.1" - cluster-key-slot "^1.1.0" - debug "^4.3.4" - denque "^2.1.0" - lodash.defaults "^4.2.0" - lodash.isarguments "^3.1.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - -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-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-potential-custom-element-name@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" - integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isomorphic-dompurify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-1.0.0.tgz#33a55b8edac845fb16cf06dfff00541eb455e13e" - integrity sha512-rxkJ2b2rwsgN/uvtaW+Z2JGfD9CYcixYMj0eTIa4V2hG47VDRMiehtUypi4lkpuwW41UrnOR65Dy0POiH3Lbjg== - dependencies: - "@types/dompurify" "^2.4.0" - dompurify "^3.0.0" - jsdom "^21.1.0" - -jmespath@0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" - integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== - -js@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/js/-/js-0.1.0.tgz#e1d0afd55ea39c2b28da304e8143eaf2c133f366" - integrity sha512-ZBbGYOpact8QAH9RprFWL4RAESYwbDodxiuDjOnzwzzk9pBzKycoifGuUrHHcDixE/eLMKPHRaXenTgu1qXBqA== - dependencies: - commander "~1.1.1" - -jsdom@^21.1.0: - version "21.1.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.0.tgz#d56ba4a84ed478260d83bd53dc181775f2d8e6ef" - integrity sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg== - dependencies: - abab "^2.0.6" - acorn "^8.8.1" - acorn-globals "^7.0.0" - cssom "^0.5.0" - cssstyle "^2.3.0" - data-urls "^3.0.2" - decimal.js "^10.4.2" - domexception "^4.0.0" - escodegen "^2.0.0" - form-data "^4.0.0" - html-encoding-sniffer "^3.0.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.1" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.2" - parse5 "^7.1.1" - saxes "^6.0.0" - symbol-tree "^3.2.4" - tough-cookie "^4.1.2" - w3c-xmlserializer "^4.0.0" - webidl-conversions "^7.0.0" - whatwg-encoding "^2.0.0" - whatwg-mimetype "^3.0.0" - whatwg-url "^11.0.0" - ws "^8.11.0" - xml-name-validator "^4.0.0" - -keypress@0.1.x: - version "0.1.0" - resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.1.0.tgz#4a3188d4291b66b4f65edb99f806aa9ae293592a" - integrity sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA== - -kuler@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" - integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== - -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== - -lodash.isarguments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== - -logform@^2.3.2, logform@^2.4.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b" - integrity sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg== - dependencies: - "@colors/colors" "1.5.0" - "@types/triple-beam" "^1.3.2" - fecha "^4.2.0" - ms "^2.1.1" - safe-stable-stringify "^2.3.1" - triple-beam "^1.3.0" - -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== - -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== - -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== - -microtime@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.1.1.tgz#f3ce12f4091c55fb2982c87205e2e7698afdcd0c" - integrity sha512-to1r7o24cDsud9IhN6/8wGmMx5R2kT0w2Xwm5okbYI3d1dk6Xv0m+Z+jg2vS9pt+ocgQHTCtgs/YuyJhySzxNg== - dependencies: - node-addon-api "^5.0.0" - node-gyp-build "^4.4.0" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, 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== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "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: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -node-addon-api@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" - integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== - -node-fetch@2: - version "2.6.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^2.6.12: - version "2.6.12" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== - dependencies: - whatwg-url "^5.0.0" - -node-gyp-build@^4.4.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" - integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== - -nwsapi@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" - integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== - -object-assign@^4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -one-time@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" - integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== - dependencies: - fn.name "1.x.x" - -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -parse5@^7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -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== - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - -prisma@4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.9.0.tgz#295954b2a89cd35a0e6bcf66b2b036dbf80c75ee" - integrity sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w== - dependencies: - "@prisma/engines" "4.9.0" - -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" - -psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== - -punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== - -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" - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -range-parser@~1.2.1: - version "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.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62" - integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-to-ms@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/readable-to-ms/-/readable-to-ms-1.0.3.tgz#7a1bc1cdf15a13e0808d488c160ea723fd04e60f" - integrity sha512-xxxnoflc1CT6st5azNw5u8m739XkrqrkYgLGTe9noe2OG5ohxz3viFQoOSXN2qdckf79da353uBPHQrsjPuYdQ== - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== - dependencies: - redis-errors "^1.0.0" - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -rolling-rate-limiter@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/rolling-rate-limiter/-/rolling-rate-limiter-0.4.2.tgz#19204ceabf4e4e421681bfe876a779d5b0d2282b" - integrity sha512-4lKCwwuINmfPvyfLtvAHn+SiwXisH8fmmEvTSHYAenAfKF0p6IErkUEIS+9dvWAslU+97WtX000ne26RAToo2w== - dependencies: - microtime "^3.0.0" - uuid "^9.0.0" - -safe-buffer@5.2.1, 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== - -safe-stable-stringify@^2.3.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz#ec7b037768098bf65310d1d64370de0dc02353aa" - integrity sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -saxes@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" - integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== - dependencies: - xmlchars "^2.2.0" - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -stack-trace@0.0.x, stack-trace@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== - -standard-as-callback@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" - integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -symbol-tree@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== - -text-hex@1.0.x: - version "1.0.0" - resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" - integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -tough-cookie@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" - integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== - dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.2.0" - url-parse "^1.5.3" - -tr46@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" - integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== - dependencies: - punycode "^2.1.1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -triple-beam@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" - integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - -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" - -universalify@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" - integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== - -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== - -url-parse@^1.5.3: - version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -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== - -w3c-xmlserializer@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" - integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== - dependencies: - xml-name-validator "^4.0.0" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webidl-conversions@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" - integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== - -whatwg-encoding@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" - integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== - dependencies: - iconv-lite "0.6.3" - -whatwg-mimetype@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" - integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== - -whatwg-url@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" - integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== - dependencies: - tr46 "^3.0.0" - webidl-conversions "^7.0.0" - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -winston-transport@^4.3.0, winston-transport@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" - integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== - dependencies: - logform "^2.3.2" - readable-stream "^3.6.0" - triple-beam "^1.3.0" - -winston@^3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50" - integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew== - dependencies: - "@colors/colors" "1.5.0" - "@dabh/diagnostics" "^2.0.2" - async "^3.2.3" - is-stream "^2.0.0" - logform "^2.4.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - safe-stable-stringify "^2.3.1" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.5.0" - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -ws@^7.4.6: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== - -ws@^8.11.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" - integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== - -ws@^8.17.1: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -xml-name-validator@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" - integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== - -xml2js@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - -xmlbuilder@~9.0.1: - version "9.0.7" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ== - -xmlchars@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" - integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==