From 4691f4ea9eecaaafd5b5a3bea94a9685f5821286 Mon Sep 17 00:00:00 2001 From: Danny Morabito Date: Tue, 5 Aug 2025 23:48:43 +0200 Subject: [PATCH] initial version --- .gitignore | 36 +++ Dockerfile | 20 ++ LICENSE | 661 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 123 +++++++++ biome.json | 31 +++ bun.lock | 58 +++++ images/logo.webp | Bin 0 -> 25136 bytes index.ts | 22 ++ package.json | 15 ++ src/main.ts | 371 ++++++++++++++++++++++++++ src/utils.ts | 113 ++++++++ tsconfig.json | 29 +++ 12 files changed, 1479 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 biome.json create mode 100644 bun.lock create mode 100644 images/logo.webp create mode 100644 index.ts create mode 100644 package.json create mode 100644 src/main.ts create mode 100644 src/utils.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53ffd00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +allowed-pubkeys.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..894158f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM oven/bun:canary-debian AS builder + +WORKDIR /app + +ADD package.json . +RUN bun install + +ADD . . + +RUN bun build index.ts --compile + +FROM debian:bookworm-slim + +WORKDIR /app + +COPY --from=builder /app/index /app/index + +EXPOSE 3000 + +CMD ["/app/index"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..efc93ac --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ + + + + + +
NIP-42 Proxy Logo +

NIP-42 Proxy

+

A simple, no-fuss NIP-42 authentication proxy for your Nostr relay.

+

+ License: AGPL v3 +

+
+ +--- + +Ever wanted to run a private Nostr relay without all the hassle? This NIP-42 proxy is your answer. It sits in front of your relay and makes sure only the right people can get in. Simple as that. + +## ✨ What's Inside? + +- **NIP-42 Auth:** Keep your relay safe and sound with NIP-42 authentication. +- **Proxy Power:** It's a straightforward proxy that forwards messages between your users and the relay. +- **Whitelist Control:** Easily manage who gets in and what they can do with a dynamic whitelist. +- **Admin Interface:** A handy RPC interface (protected by NIP-98) to manage your proxy. +- **Docker-Ready:** Comes with a `Dockerfile`, so you can get it up and running in a flash. +- **Bun-Powered:** Built with Bun for a fast and modern experience. + +## 🚀 Get Going + +### What You'll Need + +- [Bun](https://bun.sh/) +- [Docker](https://www.docker.com/) (if you're into that) + +### Let's Do This + +1. **Clone the code:** + + ```bash + git clone https://git.arx-ccn.com/Arx/nip42-proxy.git + cd nip42-proxy + ``` + +2. **Install the things:** + + ```bash + bun install + ``` + +3. **Fire it up:** + - **With Bun:** + + ```bash + RELAY_URL="wss://my-relay.com" ADMIN_PUBKEY="my-admin-pubkey" bun run index.ts + ``` + + - **With Docker:** + 1. Build it: + + ```bash + docker build -t nip42-proxy . + ``` + + 2. Run it: + + ```bash + docker run -p 3000:3000 -e RELAY_URL="wss://your-relay-url.com" -e ADMIN_PUBKEY="my-admin-pubkey" --name nip42-proxy nip42-proxy + ``` + +Now you can connect to your proxy with any Nostr client that gets NIP-42. + +## ⚙️ Tweaks and Knobs + +This proxy is configured with environment variables. Here's the rundown: + +
+ Click to see all the options + +| Variable | What it does | Default | +| ------------------------ | ----------------------------------------- | ----------- | +| `ALLOW_UNAUTHED_PUBLISH` | Let unauthenticated users publish events. | `false` | +| `RELAY_URL` | The URL of the relay you're proxying. | | +| `RELAY_OUTSIDE_URL` | The URL your users will connect to. | `RELAY_URL` | +| `RELAY_NAME` | The name of your relay. | | +| `RELAY_DESCRIPTION` | A little something about your relay. | | +| `RELAY_BANNER` | A URL for a banner image. | | +| `RELAY_ICON` | A URL for an icon. | | +| `RELAY_CONTACT` | How people can get in touch. | | +| `RELAY_POLICY` | A URL to your relay's policy. | | +| `ADMIN_PUBKEY` | The public key of the relay's admin. | | + +
+ +## 🔧 The Admin Zone + +The proxy has a special RPC interface for admins, protected by NIP-98. Just send a `POST` request to the root URL (`/`) with the right headers (`Content-Type: application/nostr+json+rpc` and a NIP-98 `Authorization` token). + +**What you can do:** + +- `supportedmethods`: See what commands are available. +- `getinfo`: Get the relay's info doc. +- `banpubkey`: Kick someone out. +- `allowpubkey`: Let someone in. +- `listallowedpubkeys`: See who's on the list. +- `allowkind`: Allow a certain kind of event. +- `disallowkind`: Block a certain kind of event. +- `listallowedkinds`: See what kinds are allowed. + +## 🤔 How's It Work? + +1. **Knock Knock:** A client connects, but they're just a guest for now. +2. **Who's There?:** The proxy asks for their credentials with an `AUTH` challenge. +3. **The Password:** The client sends back a valid `AUTH` event, signed with a key that's on the list. +4. **You're In:** The client is now authenticated and can chat with the relay. +5. **The Conversation:** Messages flow freely between the client and the relay. + +## 🤝 Wanna Help? + +Got ideas? Found a bug? Pull requests and issues are always welcome. + +## 📄 The Fine Print + +This project is licensed under the AGPLv3. Check out the [LICENSE](LICENSE) file for the full text. + diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5f36d03 --- /dev/null +++ b/biome.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..9e52ef6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,58 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "nip42-proxy", + "dependencies": { + "nostr-tools": "^2.16.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], + + "@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], + + "@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], + + "@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="], + + "@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="], + + "@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="], + + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + + "@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="], + + "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], + + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "nostr-tools": ["nostr-tools@2.16.2", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw=="], + + "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + + "@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + + "@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + + "@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + + "@scure/bip32/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], + } +} diff --git a/images/logo.webp b/images/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..ece1b65df6275a6af72a52951d80df73a60f1b59 GIT binary patch literal 25136 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 D_kRf_ literal 0 HcmV?d00001 diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..d3082e5 --- /dev/null +++ b/index.ts @@ -0,0 +1,22 @@ +import { type RelayConfig, main } from "./src/main.ts"; + +const config: RelayConfig = { + allowUnauthedPublish: Boolean(process.env.ALLOW_UNAUTHED_PUBLISH) || false, + outsideURL: process.env.RELAY_OUTSIDE_URL || process.env.RELAY_URL!, + relay: process.env.RELAY_URL!, + name: process.env.RELAY_NAME, + description: process.env.RELAY_DESCRIPTION, + banner: process.env.RELAY_BANNER, + icon: process.env.RELAY_ICON, + contact: process.env.RELAY_CONTACT, + policy: process.env.RELAY_POLICY, + adminPubkey: process.env.ADMIN_PUBKEY, +}; + +if ( + !config.relay || + (!config.relay?.startsWith("wss://") && !config.relay?.startsWith("ws://")) +) + config.relay = "wss://relay.arx-ccn.com"; + +main(config); diff --git a/package.json b/package.json new file mode 100644 index 0000000..eabe435 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "nip42-proxy", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "nostr-tools": "^2.16.2" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..46da03d --- /dev/null +++ b/src/main.ts @@ -0,0 +1,371 @@ +import type { ServerWebSocket } from "bun"; +import { nip98, type Event } from "nostr-tools"; +import { + allowKind, + allowPubkey, + banPubkey, + disallowKind, + getAllAllowedKinds, + getAllAllowedPubkeys, + isKindAllowed, + isPubkeyAllowed, + validateAuthEvent, +} from "./utils.ts"; +import { getGitCommitHash } from "./utils.ts" with { type: "macro" }; + +type Nip42ProxySocketData = { + authenticated: boolean; + authToken: string; + remoteWs?: WebSocket; +}; + +const sendMessage = ( + ws: ServerWebSocket, + message: any[], +) => ws.send(JSON.stringify(message), true); + +const sendAuth = (ws: ServerWebSocket) => { + sendMessage(ws, [ + "AUTH", + ws.data.authToken, + "This is an authenticated relay.", + ]); +}; + +export interface RelayConfig { + adminPubkey?: string; + outsideURL: string; + relay: string; + name?: string; + description?: string; + icon?: string; + banner?: string; + contact?: string; + policy?: string; + allowUnauthedPublish: boolean; +} + +export function main({ + relay, + outsideURL, + adminPubkey, + name, + description, + icon, + banner, + contact, + policy, + allowUnauthedPublish, +}: RelayConfig) { + function relayInfo(returnJson: boolean = false) { + const json = { + name, + description, + banner, + pubkey: adminPubkey, + contact, + software: "https://git.arx-ccn.com/Arx/nip42-proxy.git", + supported_nips: [1, 2, 4, 9, 11, 22, 28, 40, 42, 70, 77, 86], + version: `nip42-proxy - ${getGitCommitHash()}`, + posting_policy: policy, + icon, + limitation: { + auth_required: true, + restricted_writes: true, + }, + }; + if (returnJson) return json; + return new Response(JSON.stringify(json), { + headers: { "content-type": "application/json; charset=utf-8" }, + }); + } + + async function relayRPC( + adminPubkey: string | undefined, + token: string, + url: string, + method: string, + params: any[], + ) { + if (method === "supportedmethods") + return new Response( + JSON.stringify({ + result: [ + "supportedmethods", + "getinfo", + "banpubkey", + "allowpubkey", + "listallowedpubkeys", + "allowkind", + "disallowkind", + "listallowedkinds", + ], + }), + ); + if (method === "getinfo") + return new Response( + JSON.stringify({ + result: relayInfo(true), + }), + ); + const valid = await nip98.validateToken(token, url, method); + if (!token || !valid) + return new Response( + JSON.stringify({ + error: "You are not authorized", + }), + ); + const event = await nip98.unpackEventFromToken(token); + if (adminPubkey && event.pubkey !== adminPubkey) + return new Response( + JSON.stringify({ + error: "You are not authorized", + }), + ); + if (!(await isPubkeyAllowed(event))) + return new Response( + JSON.stringify({ + error: "You are not authorized", + }), + ); + + if (method === "allowpubkey") { + const [pubkey] = params; + if (!pubkey) + return new Response( + JSON.stringify({ + error: "Missing pubkey parameter", + }), + ); + await allowPubkey(pubkey); + return new Response( + JSON.stringify({ + result: true, + }), + ); + } + if (method === "listallowedpubkeys") { + const resp = await getAllAllowedPubkeys(); + return new Response( + JSON.stringify({ + result: resp.map((k: string) => ({ pubkey: k })), + }), + ); + } + if (method === "banpubkey") { + const [pubkey] = params; + if (!pubkey) + return new Response( + JSON.stringify({ + error: "Missing pubkey parameter", + }), + ); + await banPubkey(pubkey); + return new Response( + JSON.stringify({ + result: true, + }), + ); + } + if (method === "allowkind") { + const [kind] = params; + if (typeof kind !== "number") + return new Response( + JSON.stringify({ + error: "Missing or invalid kind parameter", + }), + ); + await allowKind(kind); + return new Response( + JSON.stringify({ + result: true, + }), + ); + } + if (method === "disallowkind") { + const [kind] = params; + if (typeof kind !== "number") + return new Response( + JSON.stringify({ + error: "Missing or invalid kind parameter", + }), + ); + await disallowKind(kind); + return new Response( + JSON.stringify({ + result: true, + }), + ); + } + if (method === "listallowedkinds") { + const resp = (await getAllAllowedKinds()).sort((a, b) => a - b); + return new Response( + JSON.stringify({ + result: resp, + }), + ); + } + return new Response( + JSON.stringify({ + error: `Unsupported method ${method}`, + }), + ); + } + + const server = Bun.serve({ + async fetch(req, server) { + const upgraded = server.upgrade(req, { + data: { + authenticated: false, + authToken: Bun.randomUUIDv7(), + }, + }); + if (upgraded) return new Response("Upgraded"); + const url = new URL(req.url); + if (url.pathname === "") url.pathname = "/"; + if (url.pathname === "/") { + if (req.headers.get("accept") === "application/nostr+json") + return relayInfo() as Response; + switch (req.headers.get("content-type")) { + case "application/nostr+json": + return relayInfo() as Response; + case "application/nostr+json+rpc": { + const token = req.headers.get("Authorization") || ""; + const data: { method?: string; params?: any[] } = await req.json(); + if (!data || !data.method) + return new Response( + JSON.stringify({ + error: "Please send a valid nostr rpc request", + }), + ); + let { method, params } = data; + params ||= []; + const urlForRequest = new URL(req.url); + urlForRequest.hostname = new URL(outsideURL).hostname; + urlForRequest.protocol = "https"; + return relayRPC( + adminPubkey, + token, + urlForRequest.toString(), + method, + params, + ); + } + default: + return new Response("Nip42 Proxy"); + } + } + if (url.pathname === "/api/health") + return new Response( + JSON.stringify({ + activeRequests: server.pendingRequests, + activeWebsockets: server.pendingWebSockets, + }), + ); + return new Response("Not Found", { status: 404 }); + }, + websocket: { + async message(ws, msg) { + const [command, ...data] = JSON.parse(msg as string); + if (!ws.data.authenticated) { + if (command === "REQ") { + const [name] = data; + sendMessage(ws, [ + "CLOSED", + name, + "auth-required: you must authenticate first", + ]); + + } else if (command === "EVENT") { + const [event] = data as [Event]; + if ( + allowUnauthedPublish && + (await isPubkeyAllowed(event)) && + (await isKindAllowed(event)) + ) { + if (ws.data.remoteWs) { + ws.data.remoteWs.send(msg); + } + return; + } + sendMessage(ws, [ + "OK", + event.id, + false, + "auth-required: you must authenticate first", + ]); + } else if (command === "AUTH") { + const [event] = data as [Event]; + const valid = await validateAuthEvent(event, ws.data.authToken); + if (!valid) { + sendMessage(ws, ["NOTICE", "Invalid auth event"]); + return; + } + sendMessage(ws, [ + "OK", + event.id, + true, + "successully authenticated. welcome", + ]); + ws.data.authenticated = true; + } else { + sendAuth(ws); + } + return; + } + + if (ws.data.authenticated && command === "AUTH") { + const [event] = data as [Event]; + sendMessage(ws, [ + "OK", + event.id, + true, + "you were already authenticated", + ]); + return; + } + + if (ws.data.remoteWs) { + const [event] = data as [Event]; + if(command === "EVENT") { + if(!await isPubkeyAllowed(event)) + return sendMessage(ws, [ + "OK", + event.id, + false, + "pubkey not allowed", + ]); + if(!await isKindAllowed(event)) + return sendMessage(ws, [ + "OK", + event.id, + false, + "kind not allowed", + ]); + } + ws.data.remoteWs.send(msg); + } + }, + open(ws) { + const remoteWs = new WebSocket(relay); + ws.data.remoteWs = remoteWs; + + remoteWs.addEventListener("message", (data) => + ws.send( + (data as MessageEvent).data as string | ArrayBufferLike, + true, + ), + ); + remoteWs.addEventListener("open", () => { + sendAuth(ws); + }); + remoteWs.addEventListener("error", console.error); + }, + perMessageDeflate: true, + close(ws) { + ws.data.remoteWs?.close(); + }, + }, + }); + console.log("Server listening @", server.url.host); +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..f637578 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,113 @@ +import type { Event } from "nostr-tools"; + +export function getGitCommitHash() { + try { + const { stdout } = Bun.spawnSync({ + cmd: ["git", "rev-parse", "HEAD"], + stdout: "pipe", + stderr: "pipe", + }); + const out = stdout?.toString()?.trim(); + return out?.split("\n")[0] || "unknown"; + } catch { + return "unknown"; + } +} + +export async function validateAuthEvent( + event: Event, + challenge: string, +): Promise { + if (event.kind !== 22242) return false; + const last30Seconds = Math.floor(Date.now() / 1000) - 30; + if (event.created_at < last30Seconds) return false; + const challengeTag = event.tags.find((tag) => tag[0] === "challenge")?.[1]; + if (challengeTag !== challenge) return false; + if(!await isPubkeyAllowed(event)) + return false; + return await isKindAllowed(event); +} + +export async function isPubkeyAllowed(event: Event): Promise { + const file = Bun.file("./allowed-pubkeys.json"); + if (!(await file.exists())) return true; + try { + const allowedPubkeys = JSON.parse(await file.text()); + return ( + Array.isArray(allowedPubkeys) && allowedPubkeys.includes(event.pubkey) + ); + } catch { + // If the file is malformed, default to deny + return false; + } +} + +export async function getAllAllowedPubkeys() { + const file = Bun.file("./allowed-pubkeys.json"); + try { + if (await file.exists()) return JSON.parse(await file.text()); + } catch {} + return [] as string[]; +} + +export async function saveAllowedPubkeys(pubkeys: string[]): Promise { + const file = Bun.file("./allowed-pubkeys.json"); + const unique = Array.from(new Set(pubkeys)); + return await file.write(JSON.stringify(unique)); +} + +export async function allowPubkey(pubkey: string): Promise { + const pubkeys = await getAllAllowedPubkeys(); + if (!pubkeys.includes(pubkey)) pubkeys.push(pubkey); + return await saveAllowedPubkeys(pubkeys); +} + + +export async function banPubkey(pubkey: string): Promise { + const pubkeys = await getAllAllowedPubkeys(); + const filtered = pubkeys.filter((p: string) => p !== pubkey); + return await saveAllowedPubkeys(filtered); +} + + +export async function isKindAllowed(event: Event): Promise { + const file = Bun.file("./allowed-kinds.json"); + if (!(await file.exists())) return true; + try { + const allowedKinds = JSON.parse(await file.text()); + if(!Array.isArray(allowedKinds)) return true; + if(allowedKinds.length === 0) return true; + return ( + Array.isArray(allowedKinds) && allowedKinds.includes(event.kind) + ); + } catch { + // If the file is malformed, default to allow + return true; + } +} + +export async function getAllAllowedKinds(): Promise { + const file = Bun.file("./allowed-kinds.json"); + try { + if (await file.exists()) return JSON.parse(await file.text()); + } catch {} + return [] as number[]; +} + +export async function saveAllowedKinds(kinds: number[]): Promise { + const file = Bun.file("./allowed-kinds.json"); + const unique = Array.from(new Set(kinds)); + return await file.write(JSON.stringify(unique)); +} + +export async function allowKind(kind: number): Promise { + const kinds = await getAllAllowedKinds(); + if (!kinds.includes(kind)) kinds.push(kind); + return await saveAllowedKinds(kinds); +} + +export async function disallowKind(kind: number): Promise { + const kinds = await getAllAllowedKinds(); + const filtered = kinds.filter((p: number) => p !== kind); + return await saveAllowedKinds(filtered); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}