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;