diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0234f80..94cb2d8 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: "@fells-code" \ No newline at end of file +github: '@fells-code' diff --git a/LICENSE b/LICENSE index bf9fc5a..162676c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,79 +1,661 @@ -GNU AFFERO GENERAL PUBLIC LICENSE -Version 3, 19 November 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (C) 2007 Free Software Foundation, Inc. - +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. -This license is identical to the GNU General Public License, except that it also -ensures that software running as a network service makes its source code -available to users. + 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. -TERMS AND CONDITIONS +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. +"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. -“Copyright” also means copyright-like laws that apply to other kinds of works, -such as semiconductor masks. +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 “Program” refers to any copyrightable work licensed under this License. -Each licensee is addressed as “you”. +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. -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. +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. -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. +7. Additional Terms. -To “convey” a work means any kind of propagation that enables other parties to -make or receive copies. +"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. -An interactive user interface displays “Appropriate Legal Notices” to the extent -that it includes a convenient and prominently visible feature that displays an -appropriate copyright notice, and 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. +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 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. +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. -15. Disclaimer of Warranty. +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. -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. +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. +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. -END OF TERMS AND CONDITIONS +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 have received a copy of the GNU Affero General Public License along -with this program. If not, see . +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 +. diff --git a/package-lock.json b/package-lock.json index 9925507..480dffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "AGPL-3.0-only", "dependencies": { "@asteasolutions/zod-to-openapi": "^8.4.3", + "@seamless-auth/types": "^0.1.3", "@sequelize/postgres": "^7.0.0-alpha.46", "@simplewebauthn/server": "^13.1.1", "@types/bcrypt": "^6.0.0", @@ -47,6 +48,7 @@ "@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/parser": "^8.46.2", "@vitest/coverage-v8": "^4.0.18", + "env-cmd": "^11.0.0", "eslint": "^10.0.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", @@ -63,9 +65,9 @@ } }, "node_modules/@asteasolutions/zod-to-openapi": { - "version": "8.4.3", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.3.tgz", - "integrity": "sha512-lwfMTN7kDbFDwMniYZUebiGGHxVGBw9ZSI4IBYjm6Ey22Kd5z/fsQb2k+Okr8WMbCCC553vi/ZM9utl5/XcvuQ==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz", + "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==", "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" @@ -117,9 +119,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { @@ -165,18 +167,28 @@ "node": ">=0.1.90" } }, + "node_modules/@commander-js/extra-typings": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", + "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "commander": "~13.1.0" + } + }, "node_modules/@commitlint/cli": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.4.4.tgz", - "integrity": "sha512-GLMNQHYGcn0ohL2HMlAnXcD1PS2vqBBGbYKlhrRPOYsWiRoLWtrewsR3uKRb9v/IdS+qOS0vqJQ64n1g8VPKFw==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", + "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/format": "^20.4.4", - "@commitlint/lint": "^20.4.4", - "@commitlint/load": "^20.4.4", - "@commitlint/read": "^20.4.4", - "@commitlint/types": "^20.4.4", + "@commitlint/format": "^20.5.0", + "@commitlint/lint": "^20.5.0", + "@commitlint/load": "^20.5.0", + "@commitlint/read": "^20.5.0", + "@commitlint/types": "^20.5.0", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, @@ -188,13 +200,13 @@ } }, "node_modules/@commitlint/config-conventional": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.4.4.tgz", - "integrity": "sha512-Usg+XXbPNG2GtFWTgRURNWCge1iH1y6jQIvvklOdAbyn2t8ajfVwZCnf5t5X4gUsy17BOiY+myszGsSMIvhOVA==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", + "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "conventional-changelog-conventionalcommits": "^9.2.0" }, "engines": { @@ -202,13 +214,13 @@ } }, "node_modules/@commitlint/config-validator": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.4.4.tgz", - "integrity": "sha512-K8hMS9PTLl7EYe5vWtSFQ/sgsV2PHUOtEnosg8k3ZQxCyfKD34I4C7FxWEfRTR54rFKeUYmM3pmRQqBNQeLdlw==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", + "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "ajv": "^8.11.0" }, "engines": { @@ -216,13 +228,13 @@ } }, "node_modules/@commitlint/ensure": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.4.4.tgz", - "integrity": "sha512-QivV0M1MGL867XCaF+jJkbVXEPKBALhUUXdjae66hes95aY1p3vBJdrcl3x8jDv2pdKWvIYIz+7DFRV/v0dRkA==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", + "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", @@ -244,13 +256,13 @@ } }, "node_modules/@commitlint/format": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.4.4.tgz", - "integrity": "sha512-jLi/JBA4GEQxc5135VYCnkShcm1/rarbXMn2Tlt3Si7DHiiNKHm4TaiJCLnGbZ1r8UfwDRk+qrzZ80kwh08Aow==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", + "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "picocolors": "^1.1.1" }, "engines": { @@ -258,13 +270,13 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.4.4.tgz", - "integrity": "sha512-y76rT8yq02x+pMDBI2vY4y/ByAwmJTkta/pASbgo8tldBiKLduX8/2NCRTSCjb3SumE5FBeopERKx3oMIm8RTQ==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", + "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "semver": "^7.6.0" }, "engines": { @@ -272,32 +284,32 @@ } }, "node_modules/@commitlint/lint": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.4.4.tgz", - "integrity": "sha512-svOEW+RptcNpXKE7UllcAsV0HDIdOck9reC2TP1QA6K5Fo0xxQV+QPjV8Zqx9g6X/hQBkF2S9ZQZ78Xrv1Eiog==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", + "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/is-ignored": "^20.4.4", - "@commitlint/parse": "^20.4.4", - "@commitlint/rules": "^20.4.4", - "@commitlint/types": "^20.4.4" + "@commitlint/is-ignored": "^20.5.0", + "@commitlint/parse": "^20.5.0", + "@commitlint/rules": "^20.5.0", + "@commitlint/types": "^20.5.0" }, "engines": { "node": ">=v18" } }, "node_modules/@commitlint/load": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.4.4.tgz", - "integrity": "sha512-kvFrzvoIACa/fMjXEP0LNEJB1joaH3q3oeMJsLajXE5IXjYrNGVcW1ZFojXUruVJ7odTZbC3LdE/6+ONW4f2Dg==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", + "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^20.4.4", + "@commitlint/config-validator": "^20.5.0", "@commitlint/execute-rule": "^20.0.0", - "@commitlint/resolve-extends": "^20.4.4", - "@commitlint/types": "^20.4.4", + "@commitlint/resolve-extends": "^20.5.0", + "@commitlint/types": "^20.5.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "is-plain-obj": "^4.1.0", @@ -319,13 +331,13 @@ } }, "node_modules/@commitlint/parse": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.4.4.tgz", - "integrity": "sha512-AjfgOgrjEozeQNzhFu1KL5N0nDx4JZmswVJKNfOTLTUGp6xODhZHCHqb//QUHKOzx36If5DQ7tci2o7szYxu1A==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", + "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" }, @@ -334,14 +346,14 @@ } }, "node_modules/@commitlint/read": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.4.4.tgz", - "integrity": "sha512-jvgdAQDdEY6L8kCxOo21IWoiAyNFzvrZb121wU2eBxI1DzWAUZgAq+a8LlJRbT0Qsj9INhIPVWgdaBbEzlF0dQ==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", + "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/top-level": "^20.4.3", - "@commitlint/types": "^20.4.4", + "@commitlint/types": "^20.5.0", "git-raw-commits": "^5.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" @@ -351,14 +363,14 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.4.4.tgz", - "integrity": "sha512-pyOf+yX3c3m/IWAn2Jop+7s0YGKPQ8YvQaxt9IQxnLIM3yZAlBdkKiQCT14TnrmZTkVGTXiLtckcnFTXYwlY0A==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", + "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^20.4.4", - "@commitlint/types": "^20.4.4", + "@commitlint/config-validator": "^20.5.0", + "@commitlint/types": "^20.5.0", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", @@ -369,16 +381,16 @@ } }, "node_modules/@commitlint/rules": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.4.4.tgz", - "integrity": "sha512-PmUp8QPLICn9w05dAx5r1rdOYoTk7SkfusJJh5tP3TqHwo2mlQ9jsOm8F0HSXU9kuLfgTEGNrunAx/dlK/RyPQ==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", + "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^20.4.4", + "@commitlint/ensure": "^20.5.0", "@commitlint/message": "^20.4.3", "@commitlint/to-lines": "^20.0.0", - "@commitlint/types": "^20.4.4" + "@commitlint/types": "^20.5.0" }, "engines": { "node": ">=v18" @@ -408,9 +420,9 @@ } }, "node_modules/@commitlint/types": { - "version": "20.4.4", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.4.4.tgz", - "integrity": "sha512-dwTGzyAblFXHJNBOgrTuO5Ee48ioXpS5XPRLLatxhQu149DFAHUcB3f0Q5eea3RM4USSsP1+WVT2dBtLVod4fg==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", + "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", "dev": true, "license": "MIT", "dependencies": { @@ -1286,20 +1298,10 @@ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", "license": "MIT" }, - "node_modules/@oxc-project/runtime": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", - "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "dev": true, "license": "MIT", "funding": { @@ -1499,9 +1501,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==", "cpu": [ "arm64" ], @@ -1516,9 +1518,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==", "cpu": [ "arm64" ], @@ -1533,9 +1535,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==", "cpu": [ "x64" ], @@ -1550,9 +1552,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==", "cpu": [ "x64" ], @@ -1567,9 +1569,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", - "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz", + "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==", "cpu": [ "arm" ], @@ -1584,9 +1586,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==", "cpu": [ "arm64" ], @@ -1604,9 +1606,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==", "cpu": [ "arm64" ], @@ -1624,9 +1626,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==", "cpu": [ "ppc64" ], @@ -1644,9 +1646,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==", "cpu": [ "s390x" ], @@ -1664,9 +1666,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==", "cpu": [ "x64" ], @@ -1684,9 +1686,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==", "cpu": [ "x64" ], @@ -1704,9 +1706,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==", "cpu": [ "arm64" ], @@ -1721,9 +1723,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", - "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz", + "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==", "cpu": [ "wasm32" ], @@ -1738,9 +1740,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==", "cpu": [ "arm64" ], @@ -1755,9 +1757,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==", "cpu": [ "x64" ], @@ -1772,9 +1774,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", - "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", + "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", "dev": true, "license": "MIT" }, @@ -1785,6 +1787,18 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@seamless-auth/types": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.1.3.tgz", + "integrity": "sha512-9Y2dq3clnXcSfoJ+pypt6FbMhXN75XLRu4+KuIFITl4OCSjuE8qudrF2nw21TG2meeadN0GHhAAYYzrng5tRoQ==", + "license": "AGPL-3.0", + "dependencies": { + "zod": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@sequelize/core": { "version": "7.0.0-alpha.48", "resolved": "https://registry.npmjs.org/@sequelize/core/-/core-7.0.0-alpha.48.tgz", @@ -2024,9 +2038,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -2140,9 +2154,9 @@ } }, "node_modules/@types/pg": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", - "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -2300,17 +2314,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -2323,22 +2337,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -2354,14 +2368,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -2376,14 +2390,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2394,9 +2408,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -2411,15 +2425,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -2436,9 +2450,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -2450,16 +2464,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2478,16 +2492,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2502,13 +2516,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2533,14 +2547,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", - "integrity": "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz", + "integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.1", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2554,8 +2568,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.0", - "vitest": "4.1.0" + "@vitest/browser": "4.1.1", + "vitest": "4.1.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2564,16 +2578,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", + "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, @@ -2582,13 +2596,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", + "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2597,7 +2611,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2609,9 +2623,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", + "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2622,13 +2636,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", + "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.1", "pathe": "^2.0.3" }, "funding": { @@ -2636,14 +2650,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2652,9 +2666,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", "dev": true, "license": "MIT", "funding": { @@ -2662,13 +2676,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, @@ -2972,9 +2986,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3277,13 +3291,13 @@ } }, "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/compare-func": { @@ -3734,6 +3748,24 @@ "node": ">= 0.8" } }, + "node_modules/env-cmd": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-11.0.0.tgz", + "integrity": "sha512-gnG7H1PlwPqsGhFJNTv68lsDGyQdK+U9DwLVitcj1+wGq7LeOBgUzZd2puZ710bHcH9NfNeGWe2sbw7pdvAqDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commander-js/extra-typings": "^13.1.0", + "commander": "^13.1.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "env-cmd": "bin/env-cmd.js" + }, + "engines": { + "node": ">=20.10.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3891,16 +3923,16 @@ } }, "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", + "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", @@ -3913,7 +3945,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4669,9 +4701,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5176,9 +5208,9 @@ } }, "node_modules/jose": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", - "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -5657,6 +5689,16 @@ "url": "https://opencollective.com/lint-staged" } }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -5958,9 +6000,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -6546,9 +6588,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6897,14 +6939,14 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", - "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz", + "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.115.0", - "@rolldown/pluginutils": "1.0.0-rc.9" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" @@ -6913,21 +6955,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-x64": "1.0.0-rc.9", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + "@rolldown/binding-android-arm64": "1.0.0-rc.11", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", + "@rolldown/binding-darwin-x64": "1.0.0-rc.11", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" } }, "node_modules/run-parallel": { @@ -7695,9 +7737,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.32.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.0.tgz", - "integrity": "sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==", + "version": "5.32.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.1.tgz", + "integrity": "sha512-6HQoo7+j8PA2QqP5kgAb9dl1uxUjvR0SAoL/WUp1sTEvm0F6D5npgU2OGCLwl++bIInqGlEUQ2mpuZRZYtyCzQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -7821,9 +7863,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -7974,16 +8016,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8103,17 +8145,16 @@ } }, "node_modules/vite": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", - "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz", + "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.9", + "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "bin": { @@ -8130,7 +8171,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.0.0-alpha.31", + "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -8182,19 +8223,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", + "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8206,7 +8247,7 @@ "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8222,13 +8263,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -8514,9 +8555,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index fd9096b..df9890b 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,13 @@ "coverage": "vitest run --coverage", "lint": "eslint . --ext .ts", "format": "prettier --write .", - "docker": "docker compose up -d", - "docker:down": "docker compose down", - "db:create": "sequelize-cli db:create", - "db:drop": "sequelize-cli db:drop", - "db:refresh": "sequelize-cli db:drop && sequelize-cli db:create", - "migrate:create": "sequelize-cli migration:generate --name", - "migrate:up": "sequelize-cli db:migrate", - "migrate:down": "sequelize-cli db:migrate:undo", - "migrate:reset": "sequelize-cli db:migrate:undo:all && sequelize-cli db:migrate", - "seed:run": "sequelize-cli db:seed:all", - "seed:undo": "sequelize-cli db:seed:undo:all" + "db:create": "env-cmd -f .env sequelize-cli db:create || sequelize-cli db:create", + "db:drop": "env-cmd -f .env sequelize-cli db:drop || sequelize-cli db:drop", + "db:refresh": "env-cmd -f .env sequelize-cli db:drop && sequelize-cli db:create", + "migrate:create": "env-cmd -f .env sequelize-cli migration:generate --name", + "migrate:up": "env-cmd -f .env sequelize-cli db:migrate || sequelize-cli db:migrate", + "migrate:down": "env-cmd -f .env sequelize-cli db:migrate:undo", + "migrate:reset": "env-cmd -f .env sequelize-cli db:migrate:undo:all && sequelize-cli db:migrate" }, "repository": { "type": "git", @@ -38,6 +34,7 @@ "homepage": "https://github.com/fells-code/seamless-auth-api#readme", "dependencies": { "@asteasolutions/zod-to-openapi": "^8.4.3", + "@seamless-auth/types": "^0.1.3", "@sequelize/postgres": "^7.0.0-alpha.46", "@simplewebauthn/server": "^13.1.1", "@types/bcrypt": "^6.0.0", @@ -75,6 +72,7 @@ "@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/parser": "^8.46.2", "@vitest/coverage-v8": "^4.0.18", + "env-cmd": "^11.0.0", "eslint": "^10.0.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", diff --git a/src/app.ts b/src/app.ts index e7d744d..4203b9a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -38,7 +38,7 @@ const corsOptions: CorsOptions = { return callback(null, true); } - if (origin === allowedOrigin) { + if (origin === allowedOrigin || origin === 'http://localhost:5174') { return callback(null, true); } @@ -110,6 +110,18 @@ export async function createApp() { return next(); }); + app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { + if (err) { + logger.error('Unhandled error', err); + + return res.status(500).json({ + error: 'Internal server error', + }); + } + + return next(); + }); + app.use((req: Request, res: Response) => { logger.warn( `[${req.ip}] didn't make it anywhere. Path: ${req.path}. Tracking of suspicous behavior`, diff --git a/src/controllers/admin.ts b/src/controllers/admin.ts new file mode 100644 index 0000000..1d63cc8 --- /dev/null +++ b/src/controllers/admin.ts @@ -0,0 +1,430 @@ +import { CreateUserSchema, UpdateUserSchema } from '@seamless-auth/types'; +import { Request, Response } from 'express'; +import { Op, WhereOptions } from 'sequelize'; + +import { AuthEvent, AuthEventAttributes } from '../models/authEvents.js'; +import { Credential } from '../models/credentials.js'; +import { sequelize } from '../models/index.js'; +import { Session } from '../models/sessions.js'; +import { User } from '../models/users.js'; +import { AuthEventQuerySchema } from '../schemas/internal.query.js'; +import { AuthEventService } from '../services/authEventService.js'; +import { hardRevokeSession } from '../services/sessionService.js'; +import { ServiceRequest } from '../types/types.js'; +import getLogger from '../utils/logger.js'; + +const logger = getLogger('admin'); + +export const getUsers = async (req: ServiceRequest, res: Response) => { + const { limit = 50, offset = 0, search } = req.query; + + const where: WhereOptions = search + ? { + [Op.or]: [ + { email: { [Op.iLike]: `%${search}%` } }, + { phone: { [Op.iLike]: `%${search}%` } }, + ], + } + : {}; + + const [users, total] = await Promise.all([ + await User.findAll({ + where, + attributes: [ + 'id', + 'email', + 'phone', + 'revoked', + 'emailVerified', + 'phoneVerified', + 'verified', + 'lastLogin', + 'roles', + 'createdAt', + 'updatedAt', + ], + limit: Number(limit), + offset: Number(offset), + }), + User.count({ where }), + ]); + + return res.json({ + users: users ?? [], + total, + }); +}; + +export const createUser = async (req: Request, res: Response) => { + const parsed = CreateUserSchema.safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ + message: 'Invalid payload', + details: parsed.error, + }); + } + + const { email, phone, roles } = parsed.data; + + try { + const existing = await User.findOne({ where: { email } }); + + if (existing) { + return res.status(409).json({ message: 'User already exists' }); + } + + const user = await User.create({ + email, + phone: phone, + roles: roles ?? [], + }); + + return res.status(201).json({ user }); + } catch (err) { + logger.error(`Failed to create user. Reason: ${err}`); + return res.status(500).json({ message: 'Failed to create user' }); + } +}; + +export const deleteUser = async (req: ServiceRequest, res: Response) => { + logger.info('Internal deletion call made.'); + const { userId } = req.body; + + try { + if (!userId) { + return res.status(404).json({ message: 'User not found.' }); + } + + try { + const user = await User.findOne({ + where: { + id: userId, + }, + }); + + if (user) { + user.destroy(); + logger.info(`User ${user.email} deleted from database through the seamless auth portal.`); + } else { + logger.error(`Failed to destory a seemingly valid user via the portal`); + } + + return res.status(200).json({ message: 'Success' }); + } catch (error: unknown) { + logger.error(`Failed to delete user: ${userId}. Error: ${error}`); + return res.status(500).json({ message: 'Failed' }); + } + } catch (error) { + logger.error(`Error occured deleting a user: ${error}`); + return res.status(500).json({ message: `Failed` }); + } +}; + +export const updateUser = async (req: ServiceRequest, res: Response) => { + const { userId } = req.params; + + if (!userId) { + logger.error('Missing user id for updating user'); + return res.status(400).json({ message: 'Bad request' }); + } + + const parsed = UpdateUserSchema.safeParse(req.body); + + if (!parsed.success || Object.keys(parsed.data).length === 0) { + logger.error(`Failed to parse update user body. ${JSON.stringify(req.body)}`); + return res.status(400).json({ + message: 'Invalid update payload', + details: parsed.error, + }); + } + + try { + const user = await User.findByPk(userId); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const before = user.toJSON(); + + try { + await user.update(parsed.data); + + await AuthEventService.log({ + type: 'internal_user_updated_by_owner', + req, + metadata: { + before, + after: parsed.data, + targetUser: userId, + }, + }); + } catch (error) { + logger.error(`Failed to update user ${error}`); + res.status(500).json({ message: 'Failed to update user' }); + return; + } + + res.status(200).json({ user }); + return; + } catch { + logger.error('Failed to find user'); + res.status(400).json({ message: 'Could not update users' }); + } +}; + +export const getUserDetail = async (req: ServiceRequest, res: Response) => { + const { userId } = req.params; + + const user = await User.findByPk(userId); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + const now = new Date(); + + const sessions = await Session.findAll({ + where: { + userId, + revokedAt: null, + expiresAt: { + [Op.gt]: now, + }, + }, + }); + + const credentials = await Credential.findAll({ + where: { userId }, + }); + + const events = await AuthEvent.findAll({ + where: { user_id: userId }, + limit: 50, + order: [['created_at', 'DESC']], + }); + + return res.json({ + user, + sessions, + credentials, + events, + }); +}; + +export const getUserAnomalies = async (req: Request, res: Response) => { + const { userId } = req.params; + + try { + const userEvents = await AuthEvent.findAll({ + where: { user_id: userId }, + attributes: ['ip_address', 'user_agent'], + }); + + const ips = [...new Set(userEvents.map((e) => e.ip_address).filter((v): v is string => !!v))]; + + const agents = [ + ...new Set(userEvents.map((e) => e.user_agent).filter((v): v is string => !!v)), + ]; + + const suspiciousEvents = await AuthEvent.findAll({ + where: { + type: { [Op.like]: '%suspicious%' }, + [Op.or]: [ + { user_id: userId }, + { ip_address: { [Op.in]: ips ?? [] } }, + { user_agent: { [Op.in]: agents ?? [] } }, + ], + }, + order: [['created_at', 'DESC']], + limit: 50, + }); + + return res.json({ + suspiciousEvents: suspiciousEvents, + relatedIps: Array.from(ips), + relatedAgents: Array.from(agents), + }); + } catch { + return res.status(500).json({ message: 'Failed to fetch anomalies' }); + } +}; + +// TODO: Need a public session return type for sessions +export const listUserSessions = async (req: Request, res: Response) => { + const { userId } = req.params; + + const now = new Date(); + + try { + const sessions = await Session.findAll({ + where: { + userId, + revokedAt: null, + expiresAt: { + [Op.gt]: now, + }, + }, + }); + + return res.json({ + sessions: sessions.map((s) => ({ + id: s.id, + deviceName: s.deviceName, + ipAddress: s.ipAddress, + userAgent: s.userAgent, + lastUsedAt: s.lastUsedAt, + expiresAt: s.expiresAt, + })), + }); + } catch (err) { + logger.error(`Failed to fetch sessions: ${err}`); + return res.status(500).json({ message: 'Failed to fetch sessions' }); + } +}; + +export const revokeAllUserSessions = async (req: Request, res: Response) => { + const { userId } = req.params; + + try { + const sessions = await Session.findAll({ + where: { + userId, + revokedAt: null, + }, + }); + + for (const session of sessions) { + await hardRevokeSession(session, 'admin_revoke_all'); + } + + logger.info(`All sessions revoked for user ${userId}`); + + return res.json({ message: 'Success' }); + } catch (err) { + logger.error(`Failed to revoke sessions: ${err}`); + return res.status(500).json({ message: 'Failed to revoke sessions' }); + } +}; + +// TODO: Need a public session return type for sessions +export const listAllSessions = async (req: Request, res: Response) => { + const { limit = 10, offset = 0 } = req.query; + + const now = new Date(); + + const where = { + revokedAt: null, + expiresAt: { + [Op.gt]: now, + }, + }; + + const [sessions, total] = await Promise.all([ + Session.findAll({ + where: where, + limit: Number(limit), + offset: Number(offset), + }), + Session.count({ where }), + ]); + + return res.json({ sessions, total }); +}; + +export const getDatabaseSize = async () => { + const [result] = await sequelize.query(` + SELECT pg_database_size(current_database()) as size + `); + + // TODO: Properly type this one day + return Number((result as { size: string }[])[0].size); +}; + +function expandType(type?: string): string[] { + if (!type) return []; + + if (type === 'login') return ['login_success', 'login_failed']; + if (type === 'otp') return ['otp_success', 'otp_failed']; + if (type === 'webauthn') return ['webauthn_login_success', 'webauthn_login_failed']; + if (type === 'magicLink') return ['magic_link_success', 'magic_link_requested']; + + if (type === 'suspicious') + return [ + 'login_suspicious', + 'otp_suspicious', + 'webauthn_login_suspicious', + 'verify_otp_suspicious', + 'service_token_suspicious', + ]; + + return [type]; +} + +export const getAuthEvents = async (req: ServiceRequest, res: Response) => { + const parsed = AuthEventQuerySchema.safeParse(req.query); + + if (!parsed.success) { + return res.status(400).json({ message: 'Invalid query params' }); + } + + const { limit, offset, userId, type, from, to } = parsed.data; + + const where: WhereOptions = {}; + + if (type) { + const rawType = req.query.type; + const raw: string[] = Array.isArray(rawType) + ? rawType.filter((v): v is string => typeof v === 'string') + : typeof rawType === 'string' + ? [rawType] + : []; + + const expanded = raw.flatMap(expandType); + + where.type = { + [Op.in]: expanded, + }; + } + + if (from || to) { + where.created_at = { + ...(from ? { [Op.gte]: new Date(from) } : {}), + ...(to ? { [Op.lte]: new Date(to) } : {}), + }; + } + + if (userId) where.user_id = userId; + + try { + const [events, total] = await Promise.all([ + AuthEvent.findAll({ + where, + order: [['created_at', 'DESC']], + limit, + offset, + }), + AuthEvent.count({ + where, + }), + ]); + + return res.json({ events, total }); + } catch (err) { + logger.error(`Failed to fetch auth events: ${err}`); + res.status(500).json({ message: 'Failed to fetch events' }); + } +}; + +export const getCredentialsCount = async (req: ServiceRequest, res: Response) => { + logger.info('Internal credential count call made.'); + try { + const credentialCount = await Credential.count(); + + return res.json({ count: credentialCount || 0 }); + } catch (err) { + logger.error(`Failed to fetch credential count: ${err}`); + res.status(500).json({ message: 'Failed to fetch credential count' }); + } +}; diff --git a/src/controllers/internalDashboard.ts b/src/controllers/internalDashboard.ts new file mode 100644 index 0000000..85faa16 --- /dev/null +++ b/src/controllers/internalDashboard.ts @@ -0,0 +1,82 @@ +import { Request, Response } from 'express'; +import { Op } from 'sequelize'; + +import { AuthEvent } from '../models/authEvents.js'; +import { Session } from '../models/sessions.js'; +import { User } from '../models/users.js'; +import { getDatabaseSize } from './admin.js'; + +export const getDashboardMetrics = async (_req: Request, res: Response) => { + const now = new Date(); + const last24h = new Date(now.getTime() - 1000 * 60 * 60 * 24); + + try { + const [ + totalUsers, + activeSessions, + newUsers24h, + loginSuccess24h, + loginFailed24h, + otpUsage24h, + passkeyUsage24h, + dbSize, + ] = await Promise.all([ + User.count(), + Session.count({ where: { revokedAt: null } }), + + User.count({ + where: { + createdAt: { [Op.gt]: last24h }, + }, + }), + + AuthEvent.count({ + where: { + type: 'login_success', + created_at: { [Op.gt]: last24h }, + }, + }), + + AuthEvent.count({ + where: { + type: 'login_failed', + created_at: { [Op.gt]: last24h }, + }, + }), + + AuthEvent.count({ + where: { + type: 'otp_success', + created_at: { [Op.gt]: last24h }, + }, + }), + + AuthEvent.count({ + where: { + type: { [Op.like]: '%webauthn_login_success%' }, + created_at: { [Op.gt]: last24h }, + }, + }), + + getDatabaseSize(), + ]); + + const totalLogins = loginSuccess24h + loginFailed24h; + + return res.json({ + totalUsers, + activeSessions, + newUsers24h, + + loginSuccess24h, + loginFailed24h, + successRate24h: totalLogins > 0 ? loginSuccess24h / totalLogins : 0, + + otpUsage24h, + passkeyUsage24h, + databaseSize: dbSize, + }); + } catch { + return res.status(500).json({ message: 'Failed to fetch dashboard metrics' }); + } +}; diff --git a/src/controllers/internalMetrics.ts b/src/controllers/internalMetrics.ts new file mode 100644 index 0000000..26c0e01 --- /dev/null +++ b/src/controllers/internalMetrics.ts @@ -0,0 +1,239 @@ +import { Request, Response } from 'express'; +import { col, fn, literal, Op, WhereOptions } from 'sequelize'; + +import { AuthEvent, AuthEventAttributes } from '../models/authEvents.js'; +import { MetricsQuerySchema } from '../schemas/internal.query.js'; +import getLogger from '../utils/logger.js'; + +const logger = getLogger('internal-metrics'); + +type TimeseriesRow = { + bucket: Date | string; + type: string; + count: string | number; +}; + +type ResultInstance = { + get(key: K): TimeseriesRow[K]; +}; + +type BucketStats = { + bucket: string; + success: number; + failed: number; +}; + +type SummaryRow = { + type: string; + count: string | number; +}; + +type SummaryResultInstance = { + get(key: K): SummaryRow[K]; +} & { + type: string; +}; + +export const getAuthEventSummary = async (req: Request, res: Response) => { + const parsed = MetricsQuerySchema.safeParse(req.query); + + if (!parsed.success) { + return res.status(400).json({ message: 'Invalid query params' }); + } + + const { from, to } = parsed.data; + + const where: WhereOptions = + from || to + ? { + created_at: { + ...(from ? { [Op.gte]: new Date(from) } : {}), + ...(to ? { [Op.lte]: new Date(to) } : {}), + }, + } + : {}; + + try { + const results = (await AuthEvent.findAll({ + attributes: ['type', [fn('COUNT', col('type')), 'count']], + where, + group: ['type'], + })) as SummaryResultInstance[]; + + return res.json({ + summary: results.map((r: SummaryResultInstance) => ({ + type: r.type, + count: Number(r.get('count')), + })), + }); + } catch (err) { + logger.error(`Failed to fetch auth summary: ${err}`); + return res.status(500).json({ message: 'Failed to fetch summary' }); + } +}; + +export const getAuthEventTimeseries = async (req: Request, res: Response) => { + const parsed = MetricsQuerySchema.safeParse(req.query); + + if (!parsed.success) { + return res.status(400).json({ message: 'Invalid query params' }); + } + + const { from, to, interval, userId } = parsed.data; + + // Default to last 24h if not provided + const now = new Date(); + const defaultFrom = new Date(now.getTime() - 1000 * 60 * 60 * 24); + + const createdAtFilter = + from || to + ? { + ...(from ? { [Op.gte]: new Date(from) } : {}), + ...(to ? { [Op.lte]: new Date(to) } : {}), + } + : { + [Op.gte]: defaultFrom, + }; + + const where: WhereOptions = { + type: { + [Op.in]: ['login_success', 'login_failed'], + }, + + ...(userId ? { user_id: userId } : {}), + + created_at: createdAtFilter, + }; + + const bucket = + interval === 'day' + ? literal(`DATE_TRUNC('day', created_at)`) + : literal(`DATE_TRUNC('hour', created_at)`); + + try { + const results = await AuthEvent.findAll({ + attributes: [[bucket, 'bucket'], 'type', [fn('COUNT', col('id')), 'count']], + where, + group: ['bucket', 'type'], + order: [[literal('bucket'), 'ASC']], + }); + + const map: Record = {}; + + for (const r of results as ResultInstance[]) { + const bucket = new Date(r.get('bucket')).toISOString(); + const type = r.get('type'); + const count = Number(r.get('count')); + + if (!map[bucket]) { + map[bucket] = { + bucket, + success: 0, + failed: 0, + }; + } + + if (type === 'login_success') { + map[bucket].success = count; + } else if (type === 'login_failed') { + map[bucket].failed = count; + } + } + + const filled: BucketStats[] = []; + + if (interval === 'day') { + for (let i = 29; i >= 0; i--) { + const d = new Date(now); + d.setUTCDate(d.getUTCDate() - i); + d.setUTCHours(0, 0, 0, 0); + + const key = d.toISOString(); + + filled.push({ + bucket: key, + success: map[key]?.success ?? 0, + failed: map[key]?.failed ?? 0, + }); + } + } else { + for (let i = 23; i >= 0; i--) { + const d = new Date(now); + d.setUTCHours(d.getUTCHours() - i, 0, 0, 0); + + const key = d.toISOString(); + + filled.push({ + bucket: key, + success: map[key]?.success ?? 0, + failed: map[key]?.failed ?? 0, + }); + } + } + + return res.json({ + timeseries: filled, + }); + } catch (err) { + logger.error(`Failed to fetch timeseries: ${err}`); + return res.status(500).json({ message: 'Failed to fetch timeseries' }); + } +}; + +export const getLoginStats = async (req: Request, res: Response) => { + try { + const success = await AuthEvent.count({ + where: { type: 'login_success' }, + }); + + const failed = await AuthEvent.count({ + where: { type: 'login_failed' }, + }); + + return res.json({ + success, + failed, + successRate: success + failed > 0 ? success / (success + failed) : 0, + }); + } catch (err) { + logger.error(`Failed to get Auth Events timeseries data. Reason: ${err}`); + return res.status(500).json({ message: 'Failed to compute login stats' }); + } +}; + +export const getGroupedEventSummary = async (_req: Request, res: Response) => { + try { + const events = await AuthEvent.findAll(); + + const grouped = { + login: 0, + otp: 0, + webauthn: 0, + magicLink: 0, + system: 0, + suspicious: 0, + other: 0, + }; + + for (const e of events) { + const type = e.type; + + if (type.includes('login')) grouped.login++; + else if (type.includes('otp')) grouped.otp++; + else if (type.includes('webauthn')) grouped.webauthn++; + else if (type.includes('magic_link')) grouped.magicLink++; + else if (type.includes('system_config')) grouped.system++; + else if (type.includes('suspicious')) grouped.suspicious++; + else grouped.other++; + } + + return res.json({ + summary: Object.entries(grouped).map(([type, count]) => ({ + type, + count, + })), + }); + } catch { + return res.status(500).json({ message: 'Failed to group events' }); + } +}; diff --git a/src/controllers/internalSecurity.ts b/src/controllers/internalSecurity.ts new file mode 100644 index 0000000..2a2aa88 --- /dev/null +++ b/src/controllers/internalSecurity.ts @@ -0,0 +1,37 @@ +import { Request, Response } from 'express'; +import { Op } from 'sequelize'; + +import { AuthEvent } from '../models/authEvents.js'; + +export const getSecurityAnomalies = async (_req: Request, res: Response) => { + const now = new Date(); + const windowStart = new Date(now.getTime() - 60 * 60 * 1000); + + try { + const failedLogins = await AuthEvent.findAll({ + where: { + type: 'login_failed', + created_at: { [Op.gte]: windowStart }, + }, + attributes: ['ip_address'], + }); + + const ipCounts: Record = {}; + + for (const event of failedLogins) { + const ip = event.ip_address || 'unknown'; + ipCounts[ip] = (ipCounts[ip] || 0) + 1; + } + + const suspicious = Object.entries(ipCounts) + .filter(([_, count]) => count > 10) + .map(([ip, count]) => ({ ip, count })); + + return res.json({ + suspiciousIps: suspicious, + totalFailedLogins: failedLogins.length, + }); + } catch { + return res.status(500).json({ message: 'Failed to detect anomalies' }); + } +}; diff --git a/src/controllers/systemConfig.ts b/src/controllers/systemConfig.ts index 33bf675..1461669 100644 --- a/src/controllers/systemConfig.ts +++ b/src/controllers/systemConfig.ts @@ -4,16 +4,15 @@ */ import { Response } from 'express'; -import { invalidateSystemConfigCache } from '../config/getSystemConfig.js'; +import { getSystemConfig, invalidateSystemConfigCache } from '../config/getSystemConfig.js'; import { SystemConfig } from '../models/systemConfig.js'; import { User } from '../models/users.js'; -import { PatchSystemConfigSchema } from '../schemas/systemConfig.patch.schema.js'; +import { createPatchSystemConfigSchema } from '../schemas/systemConfig.patch.schema.js'; import { SystemConfigSchema } from '../schemas/systemConfig.schema.js'; import { AuthEventService } from '../services/authEventService.js'; import { ServiceRequest } from '../types/types.js'; import getLogger from '../utils/logger.js'; -const UpdateSystemConfigSchema = PatchSystemConfigSchema; const logger = getLogger('systemConfig'); async function getRolesInUse(): Promise> { @@ -29,15 +28,12 @@ async function getRolesInUse(): Promise> { } export async function updateSystemConfig(req: ServiceRequest, res: Response) { - const actorId = req.triggeredBy; + logger.info('Updating system config'); + const existing = await getSystemConfig(); - logger.debug(`Updating Systeml config. Updated by ${actorId}`); + const schema = createPatchSystemConfigSchema(existing); - if (!actorId) { - return res.status(401).json({ error: 'Unauthorized' }); - } - - const parsed = UpdateSystemConfigSchema.safeParse(req.body); + const parsed = schema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ @@ -81,7 +77,6 @@ export async function updateSystemConfig(req: ServiceRequest, res: Response) { { key, value, - updatedBy: actorId, }, { transaction: tx }, ); @@ -92,7 +87,6 @@ export async function updateSystemConfig(req: ServiceRequest, res: Response) { await AuthEventService.log({ type: 'system_config_updated', - userId: actorId, req, metadata: { before: existingMap, @@ -107,12 +101,6 @@ export async function updateSystemConfig(req: ServiceRequest, res: Response) { } export async function getSystemConfigHandler(req: ServiceRequest, res: Response) { - const actorId = req.triggeredBy; - - if (!actorId) { - return res.status(401).json({ error: 'Unauthorized' }); - } - const rows = await SystemConfig.findAll(); const configObject = Object.fromEntries(rows.map((row) => [row.key, row.value])); @@ -122,7 +110,6 @@ export async function getSystemConfigHandler(req: ServiceRequest, res: Response) if (!parsed.success) { logger.error(`System config has become tainted. Critical issue.`); AuthEventService.log({ - userId: actorId, type: 'system_config_error', req, metadata: { reason: 'Failed to parse the system config schema from the database' }, @@ -134,9 +121,16 @@ export async function getSystemConfigHandler(req: ServiceRequest, res: Response) await AuthEventService.log({ type: 'system_config_read', - userId: actorId, req, }); return res.status(200).json(parsed.data); } + +export const getAvailableRoles = async (_req: Request, res: Response) => { + const config = await getSystemConfig(); + + return res.json({ + roles: config.available_roles ?? [], + }); +}; diff --git a/src/controllers/webauthn.ts b/src/controllers/webauthn.ts index b6c87f1..328b613 100644 --- a/src/controllers/webauthn.ts +++ b/src/controllers/webauthn.ts @@ -409,13 +409,13 @@ const verifyWebAuthn = async (req: Request, res: Response) => { } if (!user || !user.challenge) { - await AuthEvent.create({ - user_id: null, - type: 'login_failed', - ip_address: req.ip, - user_agent: req.headers['user-agent'], + await AuthEventService.log({ + userId: user.id, + type: 'webauthn_login_failed', + req, metadata: { reason: 'No user or user challenge' }, }); + return res.status(401).json({ message: 'Authentication failed.' }); } @@ -425,13 +425,14 @@ const verifyWebAuthn = async (req: Request, res: Response) => { if (!cred) { logger.error(`Failed to find the credental for the user ${assertionResponse.id}`); - await AuthEvent.create({ - user_id: user.id, - type: 'login_failed', - ip_address: req.ip, - user_agent: req.headers['user-agent'], + + await AuthEventService.log({ + userId: user.id, + type: 'webauthn_login_failed', + req, metadata: { reason: 'No credential' }, }); + return res.status(401).json({ message: 'Authentication failed.' }); } @@ -459,13 +460,13 @@ const verifyWebAuthn = async (req: Request, res: Response) => { if (error instanceof Error) { logger.error(`Verification failed error stack: ${error.stack}`); } - await AuthEvent.create({ - user_id: user.id, - type: 'login_failed', - ip_address: req.ip, - user_agent: req.headers['user-agent'], + await AuthEventService.log({ + userId: user.id, + type: 'webauthn_login_failed', + req, metadata: { reason: 'Incorrect passkey' }, }); + return res.status(500).json({ message: 'Internal server error' }); } @@ -475,6 +476,13 @@ const verifyWebAuthn = async (req: Request, res: Response) => { counter: verification.authenticationInfo.newCounter, }); + await AuthEventService.log({ + userId: user.id, + type: 'webauthn_login_success', + req, + metadata: { reason: 'Successful login' }, + }); + const refreshToken = generateRefreshToken(); const refreshTokenHash = await hashRefreshToken(refreshToken); const { expiresAt, idleExpiresAt } = computeSessionTimes(); diff --git a/src/lib/defineRoute.ts b/src/lib/defineRoute.ts index 5034051..f60efac 100644 --- a/src/lib/defineRoute.ts +++ b/src/lib/defineRoute.ts @@ -3,15 +3,18 @@ * Licensed under the GNU Affero General Public License v3.0 */ import { NextFunction, RequestHandler, Response, Router } from 'express'; -import { ZodTypeAny } from 'zod'; +import { ZodError, ZodTypeAny } from 'zod'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { registry } from '../openapi/registry.js'; import { CookieType } from '../services/sessionService.js'; +import getLogger from '../utils/logger.js'; import { expressToOpenAPI } from './convertPath.js'; import { InferRequest, RouteSchemas } from './routeTypes.js'; import { generateExample } from './zodExample.js'; +const logger = getLogger('defineRoute'); + type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; interface DefineRouteOptions { @@ -144,21 +147,37 @@ export function defineRoute( const originalJson = res.json.bind(res); if (response) { - const schema = - typeof response === 'object' && '200' in (response as object) - ? (response as Record)[200] - : response; - - if (schema) { - res.json = ((data: unknown) => { - const parsed = (schema as ZodTypeAny).parse(data); - return originalJson(parsed); - }) as typeof res.json; - } + res.json = ((data: unknown) => { + try { + const status = res.statusCode || 200; + + let schema: ZodTypeAny | undefined; + + if (typeof response === 'object') { + schema = (response as Record)[status]; + } else { + schema = response; + } + + if (schema) { + const parsed = schema.parse(data); + return originalJson(parsed); + } + + return originalJson(data); + } catch (err) { + logger.error('Response schema validation failed', err); + + return originalJson({ + error: 'Response validation failed', + issues: err instanceof ZodError ? err.issues : err, + }); + } + }) as typeof res.json; } - await Promise.resolve(handler(req as InferRequest, res, next)); } catch (error: unknown) { + logger.error(`Error wrapping parsed handler. ${error}`); return next(error); } }; diff --git a/src/middleware/authenticateServiceToken.ts b/src/middleware/authenticateServiceToken.ts index dd1deef..f4fe160 100644 --- a/src/middleware/authenticateServiceToken.ts +++ b/src/middleware/authenticateServiceToken.ts @@ -11,9 +11,22 @@ import { getSecret } from '../utils/secretsStore.js'; const logger = getLogger('authenticateServiceToken'); +let cachedSecret: string | null = null; + +async function getInternalSecret() { + if (cachedSecret) return cachedSecret; + cachedSecret = await getSecret('API_SERVICE_TOKEN'); + return cachedSecret; +} + export async function verifyServiceToken(req: ServiceRequest, res: Response, next: NextFunction) { - const JWT_INTERNAL = await getSecret('SEAMLESS_INTERNAL_TOKEN'); + const JWT_INTERNAL = await getInternalSecret(); const authHeader = req.headers.authorization || ''; + + if (!authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Malformed authorization header' }); + } + const token = authHeader.replace('Bearer ', ''); if (!token) { @@ -34,6 +47,10 @@ export async function verifyServiceToken(req: ServiceRequest, res: Response, nex return res.status(403).json({ error: 'Invalid token issuer' }); } + if (decoded.aud !== 'seamless-auth') { + return res.status(403).json({ error: 'Invalid audience' }); + } + req.clientId = decoded.sub; req.triggeredBy = req.params.triggeredBy; next(); diff --git a/src/middleware/requireAdmin.ts b/src/middleware/requireAdmin.ts new file mode 100644 index 0000000..216d07f --- /dev/null +++ b/src/middleware/requireAdmin.ts @@ -0,0 +1,36 @@ +/* + * Copyright © 2026 Fells Code, LLC + * Licensed under the GNU Affero General Public License v3.0 + */ +import { NextFunction, Response } from 'express'; + +import { AuthenticatedRequest } from '../types/types.js'; +import getLogger from '../utils/logger.js'; + +const logger = getLogger('requireAdmin'); + +export function requireAdmin() { + return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + try { + if (!req.clientId) { + logger.error('Admin route hit without service identity'); + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (!req.user) { + logger.error('Admin route hit without authenticated user'); + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (!req.user.roles?.includes('admin')) { + logger.warn(`User ${req.user.id} attempted admin access without admin role`); + return res.status(403).json({ error: 'Forbidden' }); + } + + next(); + } catch (err) { + logger.error(`requireAdmin failure: ${err}`); + return res.status(500).json({ error: 'Internal server error' }); + } + }; +} diff --git a/src/middleware/verifyBearerAuth.ts b/src/middleware/verifyBearerAuth.ts index 116d219..5357cf9 100644 --- a/src/middleware/verifyBearerAuth.ts +++ b/src/middleware/verifyBearerAuth.ts @@ -4,7 +4,7 @@ */ import { NextFunction, Request, Response } from 'express'; -import { validateSession } from '../services/sessionService.js'; +import { validateBearerToken } from '../services/sessionService.js'; import { AuthenticatedRequest } from '../types/types.js'; import getLogger from '../utils/logger.js'; @@ -19,7 +19,7 @@ export async function verifyBearerAuth(req: Request, res: Response, next: NextFu const token = auth.slice(7); try { - const user = await validateSession({ type: 'bearer', value: token }); + const user = await validateBearerToken(token); if (!user) { logger.error('No user found for service bearer token'); return res.status(401).json({ error: 'unauthorized' }); diff --git a/src/middleware/verifyCookieAuth.ts b/src/middleware/verifyCookieAuth.ts index 261f9ff..3f7d99a 100644 --- a/src/middleware/verifyCookieAuth.ts +++ b/src/middleware/verifyCookieAuth.ts @@ -13,9 +13,12 @@ import { User } from '../models/users.js'; import { AuthEventService } from '../services/authEventService.js'; import { CookieType, + getUserFromSession, hardRevokeSession, revokeSessionChain, - validateSession, + validateAccessToken, + validateSessionRecord, + verifyJwtWithKid, } from '../services/sessionService.js'; import { AuthenticatedRequest } from '../types/types.js'; import getLogger from '../utils/logger.js'; @@ -35,10 +38,12 @@ export function verifyCookieAuth(cookieType: CookieType = 'access') { clearAuthCookies(res); return res.status(401).json({ error: 'unauthorized' }); } - const user = await validateSession({ - type: 'cookie', - value: ephemeralCookie, - cookieType: 'ephemeral', + + const payload = await verifyJwtWithKid(ephemeralCookie, cookieType); + if (!payload) return null; + + const user = await User.findOne({ + where: { id: payload.sub, revoked: false }, }); if (!user) { @@ -55,15 +60,23 @@ export function verifyCookieAuth(cookieType: CookieType = 'access') { // Try validating existing access token first if (accessCookie) { logger.debug(`Validating access cookie`); - const user = await validateSession({ - type: 'cookie', - value: accessCookie, - cookieType: 'access', - }); + const accessCookie = cookies['seamless_access']; + + if (accessCookie) { + const tokenData = await validateAccessToken(accessCookie); + + if (tokenData) { + const session = await validateSessionRecord(tokenData.sessionId as string); + + if (session) { + const user = await getUserFromSession(session); - if (user) { - (req as AuthenticatedRequest).user = user; - return next(); + if (user) { + (req as AuthenticatedRequest).user = user; + return next(); + } + } + } } } @@ -98,13 +111,13 @@ async function performSilentRefresh(req: Request, res: Response): Promise logger.debug(msg) : false, }); diff --git a/src/routes/admin.routes.ts b/src/routes/admin.routes.ts new file mode 100644 index 0000000..83fdda2 --- /dev/null +++ b/src/routes/admin.routes.ts @@ -0,0 +1,157 @@ +import { CreateUserSchema, UpdateUserSchema } from '@seamless-auth/types'; + +import { + createUser, + deleteUser, + getAuthEvents, + getCredentialsCount, + getUserAnomalies, + getUserDetail, + getUsers, + listAllSessions, + updateUser, +} from '../controllers/admin.js'; +import { createRouter } from '../lib/createRouter.js'; +import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { verifyServiceToken } from '../middleware/authenticateServiceToken.js'; +import { requireAdmin } from '../middleware/requireAdmin.js'; +import { UserResponseSchema } from '../schemas/admin.responses.js'; +import { InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; +import { AuthEventQuerySchema, PaginationQuerySchema } from '../schemas/internal.query.js'; +import { + AuthEventsResponseSchema, + CredentialCountSchema, + UsersListResponseSchema, +} from '../schemas/internal.responses.js'; + +const adminRouter = createRouter('/admin'); + +adminRouter.get( + '/users', + { + summary: 'List users (internal)', + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + + schemas: { + response: { + 200: UsersListResponseSchema, + 500: InternalErrorSchema, + }, + }, + }, + getUsers, +); + +adminRouter.get( + '/auth-events', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + tags: ['Admin'], + schemas: { + query: AuthEventQuerySchema, + response: { + 200: AuthEventsResponseSchema, + }, + }, + }, + getAuthEvents, +); + +adminRouter.get( + '/credential-count', + { + summary: 'Get credential count', + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + + schemas: { + response: { + 200: CredentialCountSchema, + 500: InternalErrorSchema, + }, + }, + }, + getCredentialsCount, +); + +adminRouter.post( + '/users', + { + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + schemas: { + body: CreateUserSchema, + }, + }, + createUser, +); + +adminRouter.delete( + '/users', + { + summary: 'Delete user', + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + + schemas: { + response: { + 200: MessageSchema, + 500: InternalErrorSchema, + }, + }, + }, + deleteUser, +); + +adminRouter.patch( + '/users/:userId', + { + summary: 'Update user', + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + + schemas: { + body: UpdateUserSchema, + + response: { + 200: UserResponseSchema, + 400: InternalErrorSchema, + 404: InternalErrorSchema, + }, + }, + }, + updateUser, +); + +adminRouter.get( + '/users/:userId', + { + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + }, + getUserDetail, +); + +adminRouter.get( + '/users/:userId/anomalies', + { + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + }, + getUserAnomalies, +); + +adminRouter.get( + '/sessions', + { + tags: ['Admin'], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + schema: { + query: PaginationQuerySchema, + }, + }, + listAllSessions, +); + +export default adminRouter.router; diff --git a/src/routes/admin.sessions.routes.ts b/src/routes/admin.sessions.routes.ts new file mode 100644 index 0000000..136dca9 --- /dev/null +++ b/src/routes/admin.sessions.routes.ts @@ -0,0 +1,44 @@ +import { listUserSessions, revokeAllUserSessions } from '../controllers/admin.js'; +import { createRouter } from '../lib/createRouter.js'; +import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { verifyServiceToken } from '../middleware/authenticateServiceToken.js'; +import { requireAdmin } from '../middleware/requireAdmin.js'; +import { UserIdParamSchema } from '../schemas/admin.query.js'; +import { InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; +import { SessionListResponseSchema } from '../schemas/session.responses.js'; + +const adminSessionsRouter = createRouter('/admin/sessions'); + +adminSessionsRouter.get( + '/:userId', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + tags: ['Admin'], + schemas: { + params: UserIdParamSchema, + response: { + 200: SessionListResponseSchema, + 500: InternalErrorSchema, + }, + }, + }, + listUserSessions, +); + +adminSessionsRouter.delete( + '/:userId/revoke-all', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + tags: ['Admin'], + schemas: { + params: UserIdParamSchema, + response: { + 200: MessageSchema, + 500: InternalErrorSchema, + }, + }, + }, + revokeAllUserSessions, +); + +export default adminSessionsRouter.router; diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 36f9269..8be9ed1 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -6,12 +6,8 @@ import { login, logout, refreshSession } from '../controllers/authentication.js' import { createRouter } from '../lib/createRouter.js'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { LoginRequestSchema } from '../schemas/auth.requests.js'; -import { - AuthErrorSchema, - LoginSuccessSchema, - LogoutSuccessSchema, - RefreshSuccessSchema, -} from '../schemas/auth.responses.js'; +import { LoginSuccessResponseSchema } from '../schemas/auth.responses.js'; +import { ErrorSchema, InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; const authRouter = createRouter(''); @@ -25,11 +21,11 @@ authRouter.post( body: LoginRequestSchema, response: { - 200: LoginSuccessSchema, - 400: AuthErrorSchema, - 401: AuthErrorSchema, - 403: AuthErrorSchema, - 500: AuthErrorSchema, + 200: LoginSuccessResponseSchema, + 400: ErrorSchema, + 401: ErrorSchema, + 403: ErrorSchema, + 500: InternalErrorSchema, }, }, }, @@ -45,7 +41,7 @@ authRouter.get( schemas: { response: { - 200: LogoutSuccessSchema, + 200: MessageSchema, }, }, }, @@ -61,9 +57,9 @@ authRouter.post( schemas: { response: { - 200: RefreshSuccessSchema, - 401: AuthErrorSchema, - 500: AuthErrorSchema, + 200: MessageSchema, + 401: ErrorSchema, + 500: InternalErrorSchema, }, }, }, diff --git a/src/routes/internal.routes.ts b/src/routes/internal.routes.ts new file mode 100644 index 0000000..c8140af --- /dev/null +++ b/src/routes/internal.routes.ts @@ -0,0 +1,80 @@ +import { getDashboardMetrics } from '../controllers/internalDashboard.js'; +import { + getAuthEventSummary, + getAuthEventTimeseries, + getGroupedEventSummary, + getLoginStats, +} from '../controllers/internalMetrics.js'; +import { getSecurityAnomalies } from '../controllers/internalSecurity.js'; +import { createRouter } from '../lib/createRouter.js'; +import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { verifyServiceToken } from '../middleware/authenticateServiceToken.js'; +import { requireAdmin } from '../middleware/requireAdmin.js'; +import { MetricsQuerySchema } from '../schemas/internal.query.js'; + +const internalRouter = createRouter('/internal'); + +internalRouter.get( + '/auth-events/summary', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + tags: ['Internal'], + schemas: { + query: MetricsQuerySchema, + }, + }, + getAuthEventSummary, +); + +internalRouter.get( + '/auth-events/timeseries', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + tags: ['Internal'], + schemas: { + query: MetricsQuerySchema, + }, + }, + getAuthEventTimeseries, +); + +internalRouter.get( + '/auth-events/login-stats', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + tags: ['Internal'], + }, + getLoginStats, +); + +internalRouter.get( + '/security/anomalies', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + summary: 'Detect suspicious activity', + tags: ['Internal'], + }, + getSecurityAnomalies, +); + +internalRouter.get( + '/metrics/dashboard', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + summary: 'Dashboard metrics', + tags: ['Internal'], + }, + getDashboardMetrics, +); + +internalRouter.get( + '/auth-events/grouped', + { + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + summary: 'Auth Event metrics grouped', + tags: ['Internal'], + }, + getGroupedEventSummary, +); + +export default internalRouter.router; diff --git a/src/routes/magicLink.routes.ts b/src/routes/magicLink.routes.ts index d6d54da..4ae5dec 100644 --- a/src/routes/magicLink.routes.ts +++ b/src/routes/magicLink.routes.ts @@ -10,14 +10,9 @@ import { import { createRouter } from '../lib/createRouter.js'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { magicLinkEmailLimiter, magicLinkIpLimiter } from '../middleware/rateLimit.js'; +import { ErrorSchema, InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; import { MagicLinkVerifyParamsSchema } from '../schemas/magiclink.requests.js'; -import { - MagicLinkErrorSchema, - MagicLinkPollPendingSchema, - MagicLinkPollSuccessSchema, - MagicLinkRequestResponseSchema, - MagicLinkVerifyResponseSchema, -} from '../schemas/magiclink.responses.js'; +import { MagicLinkPollSuccessSchema } from '../schemas/magiclink.responses.js'; const magicLinkRouter = createRouter('/magic-link'); @@ -30,7 +25,7 @@ magicLinkRouter.get( schemas: { response: { - 200: MagicLinkRequestResponseSchema, + 200: MessageSchema, }, }, }, @@ -47,9 +42,9 @@ magicLinkRouter.get( schemas: { response: { 200: MagicLinkPollSuccessSchema, - 204: MagicLinkPollPendingSchema, - 404: MagicLinkPollPendingSchema, - 500: MagicLinkErrorSchema, + 204: MessageSchema, + 404: ErrorSchema, + 500: InternalErrorSchema, }, }, }, @@ -66,9 +61,9 @@ magicLinkRouter.get( params: MagicLinkVerifyParamsSchema, response: { - 200: MagicLinkVerifyResponseSchema, - 400: MagicLinkErrorSchema, - 500: MagicLinkErrorSchema, + 200: MessageSchema, + 400: ErrorSchema, + 500: InternalErrorSchema, }, }, }, diff --git a/src/routes/otp.routes.ts b/src/routes/otp.routes.ts index 9d56672..4c75876 100644 --- a/src/routes/otp.routes.ts +++ b/src/routes/otp.routes.ts @@ -12,13 +12,9 @@ import { } from '../controllers/otp.js'; import { createRouter } from '../lib/createRouter.js'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { ErrorSchema, InternalErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; import { VerifyOTPRequestSchema } from '../schemas/otp.requests.js'; -import { - OTPInvalidSchema, - OTPServerErrorSchema, - OTPSuccessSchema, - OTPVerifyTokenSuccessSchema, -} from '../schemas/otp.responses.js'; +import { OTPVerifyTokenSuccessSchema } from '../schemas/otp.responses.js'; const otpRouter = createRouter('/otp'); @@ -31,9 +27,9 @@ otpRouter.get( schemas: { response: { - 200: OTPSuccessSchema, - 400: OTPInvalidSchema, - 500: OTPServerErrorSchema, + 200: MessageSchema, + 400: ErrorSchema, + 500: InternalErrorSchema, }, }, }, @@ -49,9 +45,9 @@ otpRouter.get( schemas: { response: { - 200: OTPSuccessSchema, - 400: OTPInvalidSchema, - 500: OTPServerErrorSchema, + 200: MessageSchema, + 400: ErrorSchema, + 500: InternalErrorSchema, }, }, }, @@ -67,7 +63,7 @@ otpRouter.get( schemas: { response: { - 200: OTPSuccessSchema, + 200: MessageSchema, }, }, }, @@ -83,7 +79,7 @@ otpRouter.get( schemas: { response: { - 200: OTPSuccessSchema, + 200: MessageSchema, }, }, }, @@ -102,8 +98,8 @@ otpRouter.post( response: { 200: OTPVerifyTokenSuccessSchema, - 401: OTPInvalidSchema, - 500: OTPServerErrorSchema, + 401: ErrorSchema, + 500: ErrorSchema, }, }, }, @@ -122,8 +118,8 @@ otpRouter.post( response: { 200: OTPVerifyTokenSuccessSchema, - 401: OTPInvalidSchema, - 500: OTPServerErrorSchema, + 401: ErrorSchema, + 500: ErrorSchema, }, }, }, @@ -142,8 +138,8 @@ otpRouter.post( response: { 200: OTPVerifyTokenSuccessSchema, - 401: OTPInvalidSchema, - 500: OTPServerErrorSchema, + 401: ErrorSchema, + 500: ErrorSchema, }, }, }, @@ -162,8 +158,8 @@ otpRouter.post( response: { 200: OTPVerifyTokenSuccessSchema, - 401: OTPInvalidSchema, - 500: OTPServerErrorSchema, + 401: ErrorSchema, + 500: ErrorSchema, }, }, }, diff --git a/src/routes/registration.routes.ts b/src/routes/registration.routes.ts index d70eb40..3bdad85 100644 --- a/src/routes/registration.routes.ts +++ b/src/routes/registration.routes.ts @@ -4,11 +4,9 @@ */ import { register } from '../controllers/registration.js'; import { createRouter } from '../lib/createRouter.js'; +import { ErrorSchema } from '../schemas/generic.responses.js'; import { RegistrationRequestSchema } from '../schemas/registration.requests.js'; -import { - RegistrationErrorSchema, - RegistrationSuccessSchema, -} from '../schemas/registration.responses.js'; +import { RegistrationSuccessSchema } from '../schemas/registration.responses.js'; const registrationRouter = createRouter('/registration'); @@ -23,8 +21,8 @@ registrationRouter.post( response: { 200: RegistrationSuccessSchema, - 400: RegistrationErrorSchema, - 500: RegistrationErrorSchema, + 400: ErrorSchema, + 500: ErrorSchema, }, }, }, diff --git a/src/routes/session.routes.ts b/src/routes/session.routes.ts index cdac0ef..b6dc681 100644 --- a/src/routes/session.routes.ts +++ b/src/routes/session.routes.ts @@ -5,12 +5,9 @@ import { listSessions, revokeAllSessions, revokeSession } from '../controllers/sessions.js'; import { createRouter } from '../lib/createRouter.js'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { ErrorSchema, MessageSchema } from '../schemas/generic.responses.js'; import { SessionIdParamsSchema } from '../schemas/session.params.js'; -import { - SessionDeleteResponseSchema, - SessionErrorSchema, - SessionListResponseSchema, -} from '../schemas/session.responses.js'; +import { SessionListResponseSchema } from '../schemas/session.responses.js'; const sessionsRouter = createRouter('/sessions'); @@ -24,7 +21,7 @@ sessionsRouter.get( schemas: { response: { 200: SessionListResponseSchema, - 401: SessionErrorSchema, + 401: ErrorSchema, }, }, }, @@ -42,9 +39,9 @@ sessionsRouter.delete( params: SessionIdParamsSchema, response: { - 200: SessionDeleteResponseSchema, - 401: SessionErrorSchema, - 404: SessionErrorSchema, + 200: MessageSchema, + 401: ErrorSchema, + 404: ErrorSchema, }, }, }, @@ -60,8 +57,8 @@ sessionsRouter.delete( schemas: { response: { - 200: SessionDeleteResponseSchema, - 401: SessionErrorSchema, + 200: MessageSchema, + 401: ErrorSchema, }, }, }, diff --git a/src/routes/systemConfig.routes.ts b/src/routes/systemConfig.routes.ts index 0bfde80..65d85d5 100644 --- a/src/routes/systemConfig.routes.ts +++ b/src/routes/systemConfig.routes.ts @@ -2,36 +2,48 @@ * Copyright © 2026 Fells Code, LLC * Licensed under the GNU Affero General Public License v3.0 */ -import { getSystemConfigHandler, updateSystemConfig } from '../controllers/systemConfig.js'; +import { + getAvailableRoles, + getSystemConfigHandler, + updateSystemConfig, +} from '../controllers/systemConfig.js'; import { createRouter } from '../lib/createRouter.js'; +import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; import { verifyServiceToken } from '../middleware/authenticateServiceToken.js'; -import { SystemConfigParamsSchema } from '../schemas/systemConfig.params.js'; -import { PatchSystemConfigSchema } from '../schemas/systemConfig.patch.schema.js'; +import { requireAdmin } from '../middleware/requireAdmin.js'; +import { ErrorSchema, InternalErrorSchema } from '../schemas/generic.responses.js'; import { GetSystemConfigResponseSchema, InvalidPayloadSchema, - SystemConfigErrorSchema, - UnauthorizedSchema, UpdateSystemConfigResponseSchema, } from '../schemas/systemConfig.responses.js'; const systemConfigRouter = createRouter('/system-config'); systemConfigRouter.get( - '/:triggeredBy', + '/roles', + { + summary: 'Get available roles', + tags: ['SystemConfig'], + + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], + }, + getAvailableRoles, +); + +systemConfigRouter.get( + '/admin', { summary: 'Retrieve system configuration', tags: ['SystemConfig'], - middleware: [verifyServiceToken], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], schemas: { - params: SystemConfigParamsSchema, - response: { 200: GetSystemConfigResponseSchema, - 401: UnauthorizedSchema, - 500: SystemConfigErrorSchema, + 401: ErrorSchema, + 500: InternalErrorSchema, }, }, }, @@ -39,21 +51,18 @@ systemConfigRouter.get( ); systemConfigRouter.patch( - '/:triggeredBy', + '/admin', { summary: 'Update system configuration', tags: ['SystemConfig'], - middleware: [verifyServiceToken], + middleware: [verifyServiceToken, attachAuthMiddleware(), requireAdmin()], schemas: { - params: SystemConfigParamsSchema, - body: PatchSystemConfigSchema, - response: { 200: UpdateSystemConfigResponseSchema, 400: InvalidPayloadSchema, - 401: UnauthorizedSchema, + 401: ErrorSchema, }, }, }, diff --git a/src/routes/users.routes.ts b/src/routes/users.routes.ts index b6687c4..ab4d1d4 100644 --- a/src/routes/users.routes.ts +++ b/src/routes/users.routes.ts @@ -2,14 +2,13 @@ * Copyright © 2026 Fells Code, LLC * Licensed under the GNU Affero General Public License v3.0 */ +import { DeleteCredentialRequestSchema, UpdateCredentialRequestSchema } from '@seamless-auth/types'; + import { deleteCredential, deleteUser, getUser, updateCredential } from '../controllers/user.js'; import { createRouter } from '../lib/createRouter.js'; -import { - DeleteCredentialRequestSchema, - UpdateCredentialRequestSchema, -} from '../schemas/credential.request.js'; -import { MeResponseSchema } from '../schemas/me.schema.js'; -import { DeleteUserResponseSchema } from '../schemas/user.responses.js'; +import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { MessageSchema } from '../schemas/generic.responses.js'; +import { MeResponseSchema } from '../schemas/me.response.js'; const usersRouter = createRouter('/users'); @@ -33,6 +32,8 @@ usersRouter.post( tags: ['Users'], summary: 'Update credential metadata', + middleware: [attachAuthMiddleware], + schemas: { body: UpdateCredentialRequestSchema, }, @@ -47,8 +48,10 @@ usersRouter.delete( tags: ['Users'], summary: 'Delete authenticated user', + middleware: [attachAuthMiddleware], + schemas: { - response: DeleteUserResponseSchema, + response: MessageSchema, }, }, deleteUser, @@ -61,6 +64,8 @@ usersRouter.delete( tags: ['Users'], summary: 'Delete credential', + middleware: [attachAuthMiddleware], + schemas: { body: DeleteCredentialRequestSchema, }, diff --git a/src/routes/webauthn.routes.ts b/src/routes/webauthn.routes.ts index 5c7214f..91c7cba 100644 --- a/src/routes/webauthn.routes.ts +++ b/src/routes/webauthn.routes.ts @@ -10,13 +10,13 @@ import { } from '../controllers/webauthn.js'; import { createRouter } from '../lib/createRouter.js'; import { attachAuthMiddleware } from '../middleware/attachAuthMiddleware.js'; +import { ErrorSchema, InternalErrorSchema } from '../schemas/generic.responses.js'; import { WebAuthnLoginFinishSchema, WebAuthnRegisterFinishSchema, } from '../schemas/webauthn.requests.js'; import { WebAuthnChallengeSchema, - WebAuthnErrorSchema, WebAuthnTokenSuccessSchema, } from '../schemas/webauthn.responses.js'; @@ -32,8 +32,8 @@ webauthnRouter.get( schemas: { response: { 200: WebAuthnChallengeSchema, - 403: WebAuthnErrorSchema, - 500: WebAuthnErrorSchema, + 403: ErrorSchema, + 500: ErrorSchema, }, }, }, @@ -52,8 +52,8 @@ webauthnRouter.post( response: { 200: WebAuthnTokenSuccessSchema, - 403: WebAuthnErrorSchema, - 500: WebAuthnErrorSchema, + 403: ErrorSchema, + 500: ErrorSchema, }, }, }, @@ -70,9 +70,9 @@ webauthnRouter.post( schemas: { response: { 200: WebAuthnChallengeSchema, - 401: WebAuthnErrorSchema, - 403: WebAuthnErrorSchema, - 500: WebAuthnErrorSchema, + 401: ErrorSchema, + 403: ErrorSchema, + 500: InternalErrorSchema, }, }, }, @@ -91,9 +91,9 @@ webauthnRouter.post( response: { 200: WebAuthnTokenSuccessSchema, - 401: WebAuthnErrorSchema, - 403: WebAuthnErrorSchema, - 500: WebAuthnErrorSchema, + 401: ErrorSchema, + 403: ErrorSchema, + 500: InternalErrorSchema, }, }, }, diff --git a/src/schemas/admin.query.ts b/src/schemas/admin.query.ts new file mode 100644 index 0000000..7027db4 --- /dev/null +++ b/src/schemas/admin.query.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const UserIdParamSchema = z.object({ + userId: z.string(), +}); diff --git a/src/schemas/admin.responses.ts b/src/schemas/admin.responses.ts new file mode 100644 index 0000000..b9eb4d9 --- /dev/null +++ b/src/schemas/admin.responses.ts @@ -0,0 +1,6 @@ +import { UserSchema } from '@seamless-auth/types'; +import { z } from 'zod'; + +export const UserResponseSchema = z.object({ + user: UserSchema, +}); diff --git a/src/schemas/auth.responses.ts b/src/schemas/auth.responses.ts index 33795da..f9c3732 100644 --- a/src/schemas/auth.responses.ts +++ b/src/schemas/auth.responses.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const LoginSuccessSchema = z.object({ +export const LoginSuccessResponseSchema = z.object({ message: z.string(), token: z.string().optional(), sub: z.string().optional(), @@ -8,11 +8,7 @@ export const LoginSuccessSchema = z.object({ ttl: z.number().optional(), }); -export const LogoutSuccessSchema = z.object({ - message: z.string(), -}); - -export const RefreshSuccessSchema = z.object({ +export const RefreshSuccessResponseSchema = z.object({ message: z.string(), token: z.string().optional(), refreshToken: z.string().optional(), @@ -20,7 +16,3 @@ export const RefreshSuccessSchema = z.object({ ttl: z.number().optional(), refreshTtl: z.number().optional(), }); - -export const AuthErrorSchema = z.object({ - message: z.string(), -}); diff --git a/src/schemas/authEvent.types.ts b/src/schemas/authEvent.types.ts new file mode 100644 index 0000000..25adbc9 --- /dev/null +++ b/src/schemas/authEvent.types.ts @@ -0,0 +1,64 @@ +// src/schemas/authEvent.types.ts +import { z } from 'zod'; + +export const AuthEventTypeEnum = z.enum([ + 'auth_action_incremented', + 'bearer_token_failed', + 'bearer_token_success', + 'bearer_token_suspicious', + 'cookie_token_failed', + 'cookie_token_success', + 'cookie_token_suspicious', + 'informational', + 'internal_user_updated_by_owner', + 'jwks_failed', + 'jwks_success', + 'jwks_suspicious', + 'login_failed', + 'login_success', + 'login_suspicious', + 'logout_failed', + 'logout_success', + 'logout_suspicious', + 'magic_link_poll_completed_successfully', + 'magic_link_requested', + 'magic_link_success', + 'mfa_otp_failed', + 'mfa_otp_success', + 'mfa_otp_suspicious', + 'notication_sent', + 'otp_failed', + 'otp_success', + 'otp_suspicious', + 'recovery_otp_failed', + 'recovery_otp_success', + 'recovery_otp_suspicious', + 'refresh_token_failed', + 'refresh_token_success', + 'refresh_token_suspicious', + 'registration_failed', + 'registration_success', + 'registration_suspicious', + 'service_token_failed', + 'service_token_rotated', + 'service_token_success', + 'service_token_suspicious', + 'system_config_error', + 'system_config_read', + 'system_config_updated', + 'user_created', + 'user_data_failed', + 'user_data_success', + 'user_data_suspicious', + 'verify_otp_failed', + 'verify_otp_success', + 'verify_otp_suspicious', + 'webauthn_login_failed', + 'webauthn_login_success', + 'webauthn_login_suspicious', + 'webauthn_registration_failed', + 'webauthn_registration_success', + 'webauthn_registration_suspicious', +]); + +export type AuthEventType = z.infer; diff --git a/src/schemas/credential.base.ts b/src/schemas/credential.base.ts deleted file mode 100644 index eedad52..0000000 --- a/src/schemas/credential.base.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; - -import { IsoDate } from './user.base.js'; - -export const CredentialBaseSchema = z.object({ - id: z.string(), - transports: z.array(z.string()).nullable().optional(), - deviceType: z.string().nullable().optional(), - backedup: z.boolean().nullable().optional(), - counter: z.number(), - friendlyName: z.string().nullable().optional(), - lastUsedAt: IsoDate.nullable().optional(), - platform: z.string().nullable().optional(), - browser: z.string().nullable().optional(), - deviceInfo: z.string().nullable().optional(), - createdAt: IsoDate, - updatedAt: IsoDate.optional(), -}); - -export type CredentialBase = z.infer; diff --git a/src/schemas/credential.request.ts b/src/schemas/credential.request.ts deleted file mode 100644 index faeb1b6..0000000 --- a/src/schemas/credential.request.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright © 2026 Fells Code, LLC - * Licensed under the GNU Affero General Public License v3.0 - */ -import { z } from 'zod'; - -export const UpdateCredentialRequestSchema = z.object({ - id: z.string(), - - friendlyName: z.string().min(1).max(128).optional(), - - deviceInfo: z.string().max(256).optional(), -}); - -export type UpdateCredentialRequest = z.infer; - -export const DeleteCredentialRequestSchema = z.object({ - id: z.string(), -}); - -export type DeleteCredentialRequest = z.infer; diff --git a/src/schemas/error.schema.ts b/src/schemas/error.schema.ts deleted file mode 100644 index f504877..0000000 --- a/src/schemas/error.schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from 'zod'; - -export const ErrorSchema = z.object({ - message: z.string(), -}); diff --git a/src/schemas/generic.responses.ts b/src/schemas/generic.responses.ts new file mode 100644 index 0000000..007dbab --- /dev/null +++ b/src/schemas/generic.responses.ts @@ -0,0 +1,15 @@ +import z from 'zod'; + +export const MessageSchema = z.object({ + message: z.string(), +}); + +export const ErrorSchema = z.object({ + message: z.string().optional(), + error: z.string(), +}); + +export const InternalErrorSchema = z.object({ + message: z.string().optional(), + error: z.string(), +}); diff --git a/src/schemas/internal.query.ts b/src/schemas/internal.query.ts new file mode 100644 index 0000000..fc8268e --- /dev/null +++ b/src/schemas/internal.query.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +import { AuthEventTypeEnum } from './authEvent.types.js'; + +export const PaginationQuerySchema = z.object({ + limit: z.coerce.number().min(1).max(100).optional().default(50), + offset: z.coerce.number().min(0).optional().default(0), +}); + +export const AuthEventQuerySchema = z.object({ + limit: z.coerce.number().min(1).max(100).default(10), + offset: z.coerce.number().min(0).default(0), + + userId: z.string().optional(), + type: z + .union([AuthEventTypeEnum, z.string(), z.array(z.union([AuthEventTypeEnum, z.string()]))]) + .optional(), + + from: z.string().optional(), + to: z.string().optional(), +}); + +export const MetricsQuerySchema = z.object({ + userId: z.string().optional(), + from: z.string().optional(), + to: z.string().optional(), + interval: z.enum(['hour', 'day']).optional().default('hour'), +}); diff --git a/src/schemas/internal.responses.ts b/src/schemas/internal.responses.ts new file mode 100644 index 0000000..249923c --- /dev/null +++ b/src/schemas/internal.responses.ts @@ -0,0 +1,16 @@ +import { AuthEventSchema, UserSchema } from '@seamless-auth/types'; +import { z } from 'zod'; + +export const UsersListResponseSchema = z.object({ + users: z.array(UserSchema), + total: z.number(), +}); + +export const AuthEventsResponseSchema = z.object({ + events: z.array(AuthEventSchema), + total: z.number(), +}); + +export const CredentialCountSchema = z.object({ + count: z.number(), +}); diff --git a/src/schemas/magicLink.schema.ts b/src/schemas/magicLink.schema.ts index efb7308..c71e902 100644 --- a/src/schemas/magicLink.schema.ts +++ b/src/schemas/magicLink.schema.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; export const MagicLinkRequestSchema = z.object({ - email: z.string().email(), + email: z.email(), redirect_url: z.string().optional(), }); diff --git a/src/schemas/magiclink.responses.ts b/src/schemas/magiclink.responses.ts index c670471..5028323 100644 --- a/src/schemas/magiclink.responses.ts +++ b/src/schemas/magiclink.responses.ts @@ -1,13 +1,5 @@ import { z } from 'zod'; -export const MagicLinkRequestResponseSchema = z.object({ - message: z.string(), -}); - -export const MagicLinkVerifyResponseSchema = z.object({ - message: z.string(), -}); - export const MagicLinkPollSuccessSchema = z.object({ message: z.string(), token: z.string().optional(), @@ -19,11 +11,3 @@ export const MagicLinkPollSuccessSchema = z.object({ ttl: z.number().optional(), refreshTtl: z.number().optional(), }); - -export const MagicLinkPollPendingSchema = z.object({ - error: z.string(), -}); - -export const MagicLinkErrorSchema = z.object({ - message: z.string(), -}); diff --git a/src/schemas/me.response.ts b/src/schemas/me.response.ts new file mode 100644 index 0000000..a7182f5 --- /dev/null +++ b/src/schemas/me.response.ts @@ -0,0 +1,13 @@ +import { CredentialApiSchema, UserSchema } from '@seamless-auth/types'; +import { z } from 'zod'; + +export const MeResponseSchema = z.object({ + user: UserSchema.pick({ + id: true, + email: true, + phone: true, + roles: true, + lastLogin: true, + }), + credentials: z.array(CredentialApiSchema), +}); diff --git a/src/schemas/me.schema.ts b/src/schemas/me.schema.ts deleted file mode 100644 index 143f274..0000000 --- a/src/schemas/me.schema.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from 'zod'; - -import { CredentialBaseSchema } from './credential.base.js'; -import { UserBaseSchema } from './user.base.js'; - -export const MeUserSchema = UserBaseSchema.pick({ - id: true, - email: true, - phone: true, - roles: true, - lastLogin: true, -}); - -export const CredentialSchema = CredentialBaseSchema.pick({ - id: true, - transports: true, - deviceType: true, - backedup: true, - counter: true, - friendlyName: true, - lastUsedAt: true, - platform: true, - browser: true, - deviceInfo: true, - createdAt: true, -}); - -export const MeResponseSchema = z.object({ - user: MeUserSchema, - credentials: z.array(CredentialSchema), -}); diff --git a/src/schemas/otp.responses.ts b/src/schemas/otp.responses.ts index 84694ad..55641fc 100644 --- a/src/schemas/otp.responses.ts +++ b/src/schemas/otp.responses.ts @@ -1,23 +1,7 @@ import { z } from 'zod'; -export const OTPSuccessSchema = z.object({ - message: z.literal('success'), -}); - -export const OTPVerifySuccessSchema = z.object({ - message: z.literal('Success'), -}); - export const OTPVerifyTokenSuccessSchema = z.object({ - message: z.literal('Success'), + message: z.string(), token: z.string().optional(), refreshTokenHash: z.string().optional(), }); - -export const OTPInvalidSchema = z.object({ - message: z.string(), -}); - -export const OTPServerErrorSchema = z.object({ - message: z.literal('Internal server error'), -}); diff --git a/src/schemas/registration.requests.ts b/src/schemas/registration.requests.ts index be95413..1ea8ceb 100644 --- a/src/schemas/registration.requests.ts +++ b/src/schemas/registration.requests.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; export const RegistrationRequestSchema = z.object({ - email: z.string().email(), + email: z.email(), phone: z.string(), }); diff --git a/src/schemas/registration.responses.ts b/src/schemas/registration.responses.ts index e804e64..e1f7b55 100644 --- a/src/schemas/registration.responses.ts +++ b/src/schemas/registration.responses.ts @@ -6,7 +6,3 @@ export const RegistrationSuccessSchema = z.object({ token: z.string().optional(), ttl: z.string().optional(), }); - -export const RegistrationErrorSchema = z.object({ - message: z.string(), -}); diff --git a/src/schemas/session.responses.ts b/src/schemas/session.responses.ts index 0a94cf8..3d782ce 100644 --- a/src/schemas/session.responses.ts +++ b/src/schemas/session.responses.ts @@ -1,23 +1,7 @@ +import { SessionSchema } from '@seamless-auth/types'; import { z } from 'zod'; -export const SessionSchema = z.object({ - id: z.string(), - deviceName: z.string().nullable().optional(), - ipAddress: z.string().nullable().optional(), - userAgent: z.string().nullable().optional(), - lastUsedAt: z.string(), - expiresAt: z.string(), - current: z.boolean(), -}); - export const SessionListResponseSchema = z.object({ sessions: z.array(SessionSchema), -}); - -export const SessionDeleteResponseSchema = z.object({ - message: z.literal('Success'), -}); - -export const SessionErrorSchema = z.object({ - error: z.string(), + total: z.number(), }); diff --git a/src/schemas/systemConfig.params.ts b/src/schemas/systemConfig.params.ts deleted file mode 100644 index ded434e..0000000 --- a/src/schemas/systemConfig.params.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from 'zod'; - -export const SystemConfigParamsSchema = z.object({ - triggeredBy: z.string(), -}); - -export type SystemConfigParams = z.infer; diff --git a/src/schemas/systemConfig.patch.schema.ts b/src/schemas/systemConfig.patch.schema.ts index e2a5f2f..8b935c4 100644 --- a/src/schemas/systemConfig.patch.schema.ts +++ b/src/schemas/systemConfig.patch.schema.ts @@ -2,20 +2,34 @@ * Copyright © 2026 Fells Code, LLC * Licensed under the GNU Affero General Public License v3.0 */ +// src/schemas/systemConfig.patch.schema.ts import { z } from 'zod'; +import type { SystemConfig } from './systemConfig.schema.js'; import { SystemConfigSchema } from './systemConfig.schema.js'; -export const PatchSystemConfigSchema = SystemConfigSchema.partial().superRefine((data, ctx) => { - if ( - data.default_roles && - data.available_roles && - !data.default_roles.every((r) => data.available_roles!.includes(r)) - ) { - ctx.addIssue({ - path: ['default_roles'], - message: 'All default roles must exist in available_roles', - code: z.ZodIssueCode.custom, - }); - } -}); +export function createPatchSystemConfigSchema(existing: SystemConfig) { + return SystemConfigSchema.partial().superRefine((data, ctx) => { + const nextAvailable = data.available_roles ?? existing.available_roles; + const nextDefault = data.default_roles ?? existing.default_roles; + + if ( + data.available_roles && + existing.default_roles.some((r) => !data.available_roles!.includes(r)) + ) { + ctx.addIssue({ + path: ['available_roles'], + message: 'Cannot remove roles currently set as default', + code: z.ZodIssueCode.custom, + }); + } + + if (nextDefault && nextAvailable && !nextDefault.every((r) => nextAvailable.includes(r))) { + ctx.addIssue({ + path: ['default_roles'], + message: 'All default roles must exist in available_roles', + code: z.ZodIssueCode.custom, + }); + } + }); +} diff --git a/src/schemas/user.base.ts b/src/schemas/user.base.ts deleted file mode 100644 index 0807265..0000000 --- a/src/schemas/user.base.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; - -export const IsoDate = z.coerce.date().transform((d) => d.toISOString()); - -export const UserBaseSchema = z.object({ - id: z.string(), - email: z.email(), - phone: z.string().nullable().optional(), - roles: z.array(z.string()), - lastLogin: IsoDate.optional(), - createdAt: IsoDate, - updatedAt: IsoDate.optional(), -}); - -export type UserBase = z.infer; diff --git a/src/schemas/user.responses.ts b/src/schemas/user.responses.ts deleted file mode 100644 index d8bd77f..0000000 --- a/src/schemas/user.responses.ts +++ /dev/null @@ -1,5 +0,0 @@ -import z from 'zod'; - -export const DeleteUserResponseSchema = z.object({ - success: z.boolean(), -}); diff --git a/src/schemas/webauthn.responses.ts b/src/schemas/webauthn.responses.ts index 2f44a05..ac4aafb 100644 --- a/src/schemas/webauthn.responses.ts +++ b/src/schemas/webauthn.responses.ts @@ -2,12 +2,8 @@ import { z } from 'zod'; export const WebAuthnChallengeSchema = z.record(z.string(), z.unknown()); -export const WebAuthnSimpleSuccessSchema = z.object({ - message: z.literal('Success'), -}); - export const WebAuthnTokenSuccessSchema = z.object({ - message: z.literal('Success'), + message: z.string(), token: z.string().optional(), refreshToken: z.string().optional(), refreshTokenHash: z.string().optional(), @@ -20,7 +16,3 @@ export const WebAuthnTokenSuccessSchema = z.object({ ttl: z.number().optional(), refreshTtl: z.number().optional(), }); - -export const WebAuthnErrorSchema = z.object({ - message: z.string(), -}); diff --git a/src/services/authEventService.ts b/src/services/authEventService.ts index a919839..ee7df6d 100644 --- a/src/services/authEventService.ts +++ b/src/services/authEventService.ts @@ -9,63 +9,64 @@ import getLogger from '../utils/logger.js'; const logger = getLogger('authEventService'); -type AuthEventType = - | 'login_success' +export type AuthEventType = + | 'auth_action_incremented' + | 'bearer_token_failed' + | 'bearer_token_success' + | 'bearer_token_suspicious' + | 'cookie_token_failed' + | 'cookie_token_success' + | 'cookie_token_suspicious' + | 'informational' + | 'internal_user_updated_by_owner' + | 'jwks_failed' + | 'jwks_success' + | 'jwks_suspicious' | 'login_failed' + | 'login_success' | 'login_suspicious' - | 'registration_success' - | 'registration_failed' - | 'registration_suspicious' - | 'webauthn_registration_success' - | 'webauthn_registration_failed' - | 'webauthn_registration_suspicious' - | 'webauthn_login_success' - | 'webauthn_login_failed' - | 'webauthn_login_suspicious' - | 'logout_success' | 'logout_failed' + | 'logout_success' | 'logout_suspicious' - | 'jwks_success' - | 'jwks_failed' - | 'jwks_suspicious' - | 'otp_success' - | 'otp_failed' - | 'otp_suspicious' - | 'verify_otp_success' - | 'verify_otp_failed' - | 'verify_otp_suspicious' - | 'mfa_otp_success' + | 'magic_link_poll_completed_successfully' + | 'magic_link_requested' + | 'magic_link_success' | 'mfa_otp_failed' + | 'mfa_otp_success' | 'mfa_otp_suspicious' - | 'recovery_otp_success' + | 'notication_sent' + | 'otp_failed' + | 'otp_success' + | 'otp_suspicious' | 'recovery_otp_failed' + | 'recovery_otp_success' | 'recovery_otp_suspicious' - | 'user_created' - | 'user_data_success' - | 'user_data_failed' - | 'user_data_suspicious' - | 'service_token_success' - | 'service_token_failed' - | 'service_token_suspicious' - | 'refresh_token_success' | 'refresh_token_failed' + | 'refresh_token_success' | 'refresh_token_suspicious' + | 'registration_failed' + | 'registration_success' + | 'registration_suspicious' + | 'service_token_failed' | 'service_token_rotated' - | 'bearer_token_success' - | 'bearer_token_failed' - | 'bearer_token_suspicious' - | 'cookie_token_success' - | 'cookie_token_failed' - | 'cookie_token_suspicious' - | 'auth_action_incremented' - | 'system_config_updated' + | 'service_token_success' + | 'service_token_suspicious' | 'system_config_error' | 'system_config_read' - | 'notication_sent' - | 'magic_link_requested' - | 'magic_link_success' - | 'magic_link_poll_completed_successfully' - | 'informational'; + | 'system_config_updated' + | 'user_created' + | 'user_data_failed' + | 'user_data_success' + | 'user_data_suspicious' + | 'verify_otp_failed' + | 'verify_otp_success' + | 'verify_otp_suspicious' + | 'webauthn_login_failed' + | 'webauthn_login_success' + | 'webauthn_login_suspicious' + | 'webauthn_registration_failed' + | 'webauthn_registration_success' + | 'webauthn_registration_suspicious'; export interface AuthEventOptions { userId?: string | null; diff --git a/src/services/sessionService.ts b/src/services/sessionService.ts index ad3cfc5..453b0d7 100644 --- a/src/services/sessionService.ts +++ b/src/services/sessionService.ts @@ -3,7 +3,7 @@ * Licensed under the GNU Affero General Public License v3.0 */ import { importSPKI, jwtVerify } from 'jose'; -import jwt, { JwtPayload } from 'jsonwebtoken'; +import jwt from 'jsonwebtoken'; import { Session } from '../models/sessions.js'; import { User } from '../models/users.js'; @@ -15,6 +15,14 @@ const logger = getLogger('sessionService'); export type CookieType = 'ephemeral' | 'access'; +let cachedSecret: string | null = null; + +async function getInternalSecret() { + if (cachedSecret) return cachedSecret; + cachedSecret = await getSecret('API_SERVICE_TOKEN'); + return cachedSecret; +} + export interface ValidateSessionInput { type: 'cookie' | 'bearer'; value: string; @@ -23,7 +31,7 @@ export interface ValidateSessionInput { const ISSUER = process.env.ISSUER!; -async function verifyJwtWithKid(token: string, expectedType?: 'access' | 'ephemeral') { +export async function verifyJwtWithKid(token: string, expectedType?: 'access' | 'ephemeral') { try { const { payload } = await jwtVerify( token, @@ -72,104 +80,6 @@ async function verifyJwtWithKid(token: string, expectedType?: 'access' | 'epheme } } -export async function validateSession({ - type, - value, - cookieType = 'access', -}: { - type: 'cookie' | 'bearer'; - value: string; - cookieType?: 'access' | 'ephemeral'; -}): Promise { - try { - let payload: JwtPayload | string | null = null; - - if (type === 'cookie') { - payload = await verifyJwtWithKid(value, cookieType); - if (!payload) return null; - - if (cookieType === 'ephemeral') { - const user = await User.findOne({ - where: { id: payload.sub, revoked: false }, - }); - return user ?? null; - } - - if (cookieType === 'access') { - const { sub: userId, sid: sessionId, typ } = payload; - - if (!userId || !sessionId || typ !== 'access') { - logger.warn('Access token missing required claims'); - return null; - } - - const session = await Session.findByPk(sessionId); - if (!session) { - logger.warn(`No session found for sid=${sessionId}`); - return null; - } - - const now = new Date(); - - if (session.revokedAt) { - logger.warn(`Session ${sessionId} revoked`); - return null; - } - - if (session.replacedBySessionId) { - logger.warn(`Session ${sessionId} rotated → reuse detected`); - await revokeSessionChain(session); - return null; - } - - if (session.expiresAt < now) { - logger.warn(`Session ${sessionId} expired`); - return null; - } - - if (session.idleExpiresAt < now) { - logger.warn(`Session ${sessionId} idle timeout`); - return null; - } - - const user = await User.findOne({ - where: { id: userId, revoked: false }, - }); - return user ?? null; - } - } - - if (type === 'bearer') { - const serviceSecret = await getSecret('API_SERVICE_TOKEN'); - - try { - payload = jwt.verify(value, serviceSecret, { - issuer: process.env.APP_ORIGIN, - audience: process.env.ISSUER, - }); - } catch (err: Error | unknown) { - if (err instanceof Error && err.name === 'TokenExpiredError') { - logger.info(`Expired bearer token`); - } else { - logger.error(`Bearer token verification error: ${err}`); - } - return null; - } - - const user = await User.findOne({ - where: { id: payload.sub as string, revoked: false }, - }); - - return user ?? null; - } - - return null; - } catch (err) { - console.error('[validateSession] failed:', err); - return null; - } -} - export async function revokeSessionChain(session: Session, reason = 'refresh_token_reuse') { const now = new Date(); const seen = new Set(); @@ -191,3 +101,70 @@ export async function hardRevokeSession(session: Session, reason = 'manual_revok session.revokedReason = reason; await session.save(); } + +export async function validateAccessToken(token: string) { + const payload = await verifyJwtWithKid(token, 'access'); + if (!payload) return null; + + const { sub: userId, sid: sessionId } = payload; + + if (!userId || !sessionId) return null; + + return { + userId, + sessionId, + roles: payload.roles || [], + }; +} + +export async function validateSessionRecord(sessionId: string) { + const session = await Session.findByPk(sessionId); + if (!session) return null; + + const now = new Date(); + + if (session.revokedAt) return null; + + if (session.replacedBySessionId) { + await revokeSessionChain(session); + return null; + } + + if (session.expiresAt < now) return null; + if (session.idleExpiresAt < now) return null; + + return session; +} + +export async function getUserFromSession(session: Session) { + const user = await User.findOne({ + where: { id: session.userId, revoked: false }, + }); + + return user ?? null; +} + +export async function validateBearerToken(token: string) { + const serviceSecret = await getInternalSecret(); + let payload; + + try { + payload = jwt.verify(token, serviceSecret, { + issuer: process.env.APP_ORIGIN, + audience: process.env.ISSUER, + }); + } catch (err: Error | unknown) { + if (err instanceof Error && err.name === 'TokenExpiredError') { + logger.info(`Expired bearer token`); + } else { + logger.error(`Bearer token verification error: ${err}`); + } + return null; + } + + const user = await User.findOne({ + where: { id: payload.sub as string, revoked: false }, + }); + + return user ?? null; +} diff --git a/src/types/types.ts b/src/types/types.ts index 0871003..2514d1c 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -10,6 +10,7 @@ import { User } from '../models/users.js'; export interface AuthenticatedRequest extends Request { user: User; sessionId: Session['id']; + clientId?: string; } export interface ServiceRequest extends Request { clientId?: string | (() => string); diff --git a/src/utils/signingKeyStore.ts b/src/utils/signingKeyStore.ts index 329f17c..e704239 100644 --- a/src/utils/signingKeyStore.ts +++ b/src/utils/signingKeyStore.ts @@ -61,7 +61,7 @@ function ensureDevKeys() { async function loadProdSigningKey(): Promise { const now = Date.now(); - logger.info('Refreshing signing key from Secrets Manager'); + logger.info('Refreshing signing key from env'); const activeKid = await getSecret(`${jwksPrefix}_ACTIVE_KID`); const privateKeySecretName = `${jwksPrefix}_KEY_${activeKid}_PRIVATE`; @@ -124,7 +124,6 @@ export async function getPublicKeyByKid(kid: string): Promise { return cached.pem; } - // TTL expired or not in cache → reload entire secret await loadAllPublicKeys(); return publicKeyCache[kid]?.pem ?? null;