From 5d2c1ffb282a51e5d3af76d701038c19dd5af3ac Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:09:09 -0700 Subject: [PATCH 1/4] chore: use the Interchange LICENSE text and add the CLA --- .github/workflows/cla.yml | 53 +++ CLA.md | 64 ++++ LICENSE | 673 ++++++++++---------------------------- 3 files changed, 291 insertions(+), 499 deletions(-) create mode 100644 .github/workflows/cla.yml create mode 100644 CLA.md diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..a03ad39 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,53 @@ +name: CLA Assistant + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +concurrency: + group: cla-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} + cancel-in-progress: true + +jobs: + cla: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Maintainer fast path + if: >- + github.event_name == 'pull_request_target' && + (contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) || + endsWith(github.event.pull_request.user.login, '[bot]')) + run: echo "Maintainer or bot pull request; CLA not required." + - name: CLA Assistant + if: >- + ((github.event.comment.body == 'recreate-signatures' || + github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || + github.event_name == 'pull_request_target') && + !(github.event_name == 'pull_request_target' && + (contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) || + endsWith(github.event.pull_request.user.login, '[bot]'))) + # corbitsdev/cla-assistant-action v2.6.1-node24: upstream v2.6.1 on node24. + uses: corbitsdev/cla-assistant-action@ef6d3e51db8232fe93090810f13bde30497c1d74 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-document: "https://github.com/${{ github.repository }}/blob/main/CLA.md" + path-to-signatures: "signatures/version1/cla.json" + branch: "cla-signatures" + allowlist: TheGreatAxios,brianjfox,*[bot] + custom-notsigned-prcomment: >- + Thank you for your contribution. Before it can be merged, please read our + [Contributor License Agreement](https://github.com/${{ github.repository }}/blob/main/CLA.md) + and sign it by posting a new comment on this pull request containing + exactly the line below (nothing else): + custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" + custom-allsigned-prcomment: "All contributors have signed the CLA." diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..201eec8 --- /dev/null +++ b/CLA.md @@ -0,0 +1,64 @@ +# Contributor License Agreement (CLA) + +Version 1.0 + +This Contributor License Agreement ("Agreement") is entered into between the contributor ("Contributor") and ABK Labs, Inc. ("Maintainer"). + +By submitting any Contribution to a project maintained by Maintainer, the Contributor agrees to the following terms. + +## 1. Definitions + +"Contribution" means any source code, object code, documentation, test cases, designs, specifications, bug fixes, enhancements, comments, pull requests, commits, issues containing code, or other materials intentionally submitted to a project maintained by Maintainer. + +## 2. Copyright Ownership + +The Contributor retains ownership of the Contributor's copyrights in the Contribution. + +No transfer of copyright ownership is required by this Agreement. + +## 3. License Grant to Maintainer + +The Contributor grants Maintainer a perpetual, worldwide, non-exclusive, irrevocable, royalty-free license to: + +- use, reproduce, modify, distribute, display, perform, and sublicense the Contribution; +- combine the Contribution with other software and works; +- distribute the Contribution under the project's current license; +- distribute the Contribution under future versions of the project's license; +- distribute the Contribution under alternative open-source, source-available, commercial, proprietary, or dual-license terms. + +## 4. License Grant to the Public + +The Contributor agrees that the Contribution may be distributed as part of the applicable project under the project's then-current license terms, including LGPL-2.1 or any successor license adopted by Maintainer. + +## 5. Contributor Representations + +The Contributor represents and warrants that: + +1. The Contributor created the Contribution or otherwise has sufficient rights to submit it. +2. The Contributor has the legal authority to grant the rights described in this Agreement. +3. To the best of the Contributor's knowledge, the Contribution does not knowingly infringe the intellectual property rights of any third party. +4. Any third-party material included in the Contribution has been properly disclosed and is compatible with the rights granted under this Agreement. + +## 6. Corporate Contributions + +If a Contribution is submitted on behalf of an employer or other legal entity, the person accepting this Agreement represents that they are authorized to bind that entity to this Agreement. + +## 7. No Obligation + +Maintainer is not obligated to use, distribute, maintain, support, or accept any Contribution. + +## 8. Disclaimer + +Except as expressly stated in this Agreement, the Contribution is provided "AS IS" without warranties or conditions of any kind. + +## 9. Electronic Acceptance + +The Contributor agrees that electronic acceptance of this Agreement, including acceptance through a website, source-control platform, click-through process, pull-request workflow, or similar mechanism, shall have the same force and effect as a handwritten signature. + +## 10. Governing Law + +This Agreement shall be governed by the laws of the State of California, excluding its conflict-of-law provisions. + +--- + +By submitting a Contribution, the Contributor acknowledges that they have read and agree to this Agreement. diff --git a/LICENSE b/LICENSE index f6683e7..c6487f4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,501 +1,176 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -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 this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -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 -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser 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 Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "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 -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY 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 -LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey 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 library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, see . - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Moe Ghoul, President of Vice +GNU LESSER GENERAL PUBLIC LICENSE +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +GNU LESSER GENERAL PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice That's all there is to it! From 6013146c8b5f48351bd75e20ea888692a761d6b1 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:09:10 -0700 Subject: [PATCH 2/4] build: align package.json and tsconfig with the package standard --- .gitignore | 6 +++--- e2e/helpers.ts | 4 ++-- e2e/sse-stream.test.ts | 27 +++++++++++++++------------ e2e/sse.test.ts | 2 +- package.json | 10 +++++----- src/frame.test.ts | 2 +- src/migrations.ts | 2 +- tsconfig.build.json | 7 ++----- tsconfig.json | 16 ++++++++++++---- 9 files changed, 42 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 1d90543..6f17218 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ node_modules/ dist/ -.env -.env.local *.tsbuildinfo +*.tgz coverage/ +.env +.env.* .DS_Store -*.tgz diff --git a/e2e/helpers.ts b/e2e/helpers.ts index c83660b..9858470 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -223,7 +223,7 @@ export function testTenant(id: string): TenantRow { } /** Headers for a request made as `principalId` in `tenantId`. */ -export function as(tenantId: string, principalId: string): Headers { +export function as(tenantId: string, principalId: string) { return new Headers({ [TENANT_HEADER]: tenantId, [PRINCIPAL_HEADER]: principalId, @@ -231,7 +231,7 @@ export function as(tenantId: string, principalId: string): Headers { } /** `as`, for a JSON request body. */ -export function jsonAs(tenantId: string, principalId: string): Headers { +export function jsonAs(tenantId: string, principalId: string) { const headers = as(tenantId, principalId); headers.set("content-type", "application/json"); return headers; diff --git a/e2e/sse-stream.test.ts b/e2e/sse-stream.test.ts index 3f83d80..8546a50 100644 --- a/e2e/sse-stream.test.ts +++ b/e2e/sse-stream.test.ts @@ -1,6 +1,10 @@ import { describe, expect, spyOn, test } from "bun:test"; import { SSEStreamingApi } from "hono/streaming"; -import { createMailboxRoutes, MAX_PENDING_SSE_EVENTS } from "../src/mount.js"; +import { + createMailboxRoutes, + MAX_PENDING_SSE_EVENTS, + type CreateMailboxRoutesDeps, +} from "../src/mount.js"; import { createInMemoryMailboxEventBus, type MailboxEventBus, @@ -18,17 +22,16 @@ function routes( scope: MailboxEventScope, heartbeatIntervalMs?: number, ) { - return mountAs( - scope, - createMailboxRoutes({ - db, - requireGrant: allowAllGrants, - bus, - senderAddressFor: () => `sender@${scope.tenantId}.example`, - deliver: () => {}, - heartbeatIntervalMs, - }), - ); + const deps: CreateMailboxRoutesDeps = { + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => `sender@${scope.tenantId}.example`, + deliver: () => {}, + }; + if (heartbeatIntervalMs !== undefined) + deps.heartbeatIntervalMs = heartbeatIntervalMs; + return mountAs(scope, createMailboxRoutes(deps)); } /** Read frames until `done(text)` is satisfied, or give up after `timeoutMs`. */ diff --git a/e2e/sse.test.ts b/e2e/sse.test.ts index dc10f06..d7a2d0a 100644 --- a/e2e/sse.test.ts +++ b/e2e/sse.test.ts @@ -47,7 +47,7 @@ function countingBus(): { bus: MailboxEventBus; live: () => number } { } async function readUntil( - reader: ReadableStreamDefaultReader, + reader: Pick, "read">, done: (text: string) => boolean, ): Promise { const decoder = new TextDecoder(); diff --git a/package.json b/package.json index 8de50fe..c10bc79 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,6 @@ ], "type": "module", "sideEffects": false, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", @@ -45,11 +43,12 @@ "build": "rm -rf dist && tsc -p tsconfig.build.json", "prepack": "bun run build", "typecheck": "tsc --noEmit", - "test": "bun test ./src", - "test:e2e": "bun test ./e2e", "lint": "oxlint", "format": "oxfmt", - "format:check": "oxfmt --check" + "format:check": "oxfmt --check", + "test": "bun test src --pass-with-no-tests", + "test:e2e": "bun test e2e", + "check": "bun run typecheck && bun run lint && bun run format:check && bun run test" }, "dependencies": { "arktype": "^2.2.3" @@ -90,6 +89,7 @@ "postgres": "^3.4.0" }, "engines": { + "bun": ">=1.2.0", "node": ">=24" } } diff --git a/src/frame.test.ts b/src/frame.test.ts index f0f8821..cf09c3f 100644 --- a/src/frame.test.ts +++ b/src/frame.test.ts @@ -73,7 +73,7 @@ describe("buildMailFrame headers", () => { "", "", ]; - const raw = frame({ references: chain, inReplyTo: chain[2] }); + const raw = frame({ references: chain, inReplyTo: chain[2]! }); const text = new TextDecoder().decode(raw); // Folded: continuation lines begin with the single space RFC 2822 requires. expect(text).toContain("\r\n <"); diff --git a/src/migrations.ts b/src/migrations.ts index 15209ac..112086d 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -70,7 +70,7 @@ export async function runMailboxMigrations( user: config.user, password: config.password, database: config.database, - ssl: config.ssl, + ssl: config.ssl ?? false, max: 1, onnotice: () => undefined, }); diff --git a/tsconfig.build.json b/tsconfig.build.json index 9998a21..af6694f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,15 +1,12 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": ["node"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowImportingTsExtensions": false, - "composite": false, "noEmit": false, "declaration": true, "declarationMap": false, "sourceMap": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src" }, diff --git a/tsconfig.json b/tsconfig.json index 61aa9c2..dda942c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,21 @@ { "compilerOptions": { - "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", + "moduleDetection": "force", + "isolatedModules": true, "verbatimModuleSyntax": true, - "skipLibCheck": true, + "resolveJsonModule": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, "noEmit": true, - "types": ["node", "@types/bun"] + "lib": ["ESNext"], + "types": ["bun"] }, - "exclude": ["**/node_modules", "**/dist"] + "include": ["src", "e2e"] } From c75b667fa3134a8df308923f9ab52fb87acaea94 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:09:16 -0700 Subject: [PATCH 3/4] ci: run check and a Node pack smoke --- .github/workflows/ci.yml | 37 +++++++++++++++++++ .github/workflows/test.yml | 74 -------------------------------------- e2e/helpers.ts | 2 +- 3 files changed, 38 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..27fe264 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: ci + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 15 + services: + postgres: + image: postgres:16 + env: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: mailbox_core } + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + env: + MAILBOX_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/mailbox_core + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run check + - run: bun run test:e2e + - name: node consumer smoke + run: | + set -euo pipefail + TARBALL="$PWD/$(npm pack --silent)" + mkdir -p "$RUNNER_TEMP/c" && cd "$RUNNER_TEMP/c" + npm init -y >/dev/null && npm pkg set type=module >/dev/null + npm install "$TARBALL" + node -e 'import("@corbits/mailbox").then((m) => { for (const n of ["createMailboxRoutes", "runMailboxMigrations"]) if (typeof m[n] !== "function") throw new Error("missing export: " + n); })' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index bddfed8..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: test - -on: - push: - branches: [main] - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:16 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: mailbox_core - ports: ["5433:5432"] - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - env: - MAILBOX_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5433/mailbox_core - - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.3.14 - - - run: bun install --frozen-lockfile - - run: bun run lint - - run: bun run format:check - - # Emits dist/, which is what the typecheck and the Node consumer smoke - # test resolve @corbits/mailbox through — so everything downstream proves - # the published artifact, not just the sources. - - name: build - run: bun run build - - - name: typecheck - run: bun run typecheck - - - name: unit tests - run: bun run test - - - name: e2e tests - run: bun run test:e2e - - # A consumer on plain Node must be able to install and import the - # tarball; Node cannot strip types, so a src-pointing manifest would die - # here rather than after publish. Pinned to the engines floor (Node 24). - - uses: actions/setup-node@v4 - with: - node-version: 24 - - - name: node consumer smoke test - run: | - set -euo pipefail - TARBALL="$PWD/$(npm pack --silent)" - mkdir -p "$RUNNER_TEMP/consumer" && cd "$RUNNER_TEMP/consumer" - npm init -y >/dev/null && npm pkg set type=module >/dev/null - npm install "$TARBALL" - node -e ' - import("@corbits/mailbox").then((m) => { - for (const name of ["createMailboxRoutes", "runMailboxMigrations"]) { - if (typeof m[name] !== "function") throw new Error(`missing export: ${name}`); - } - console.log("node consumer ok"); - }); - ' diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 9858470..4ba431d 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -16,7 +16,7 @@ import type { ResolvedPrincipal } from "../src/mount.js"; export const TEST_DATABASE_URL = process.env.MAILBOX_TEST_DATABASE_URL ?? - "postgres://postgres:postgres@localhost:5433/mailbox_core"; + "postgres://postgres:postgres@localhost:5432/mailbox_core"; /** * Opens a standalone handle. `close` drains the pool — without it a suite From 510bc15283810107a325cdbfbc28a417d5e303c5 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 20:09:16 -0700 Subject: [PATCH 4/4] docs: trim CONTRIBUTING, add AGENTS.md, drop ARCHITECTURE.md --- AGENTS.md | 45 ++++ ARCHITECTURE.md | 531 ------------------------------------------------ CHANGELOG.md | 179 ---------------- CONTRIBUTING.md | 109 ++-------- README.md | 2 - 5 files changed, 65 insertions(+), 801 deletions(-) create mode 100644 AGENTS.md delete mode 100644 ARCHITECTURE.md delete mode 100644 CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e1efd36 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# AGENTS.md + +## Purpose + +`@corbits/mailbox` is a native `@intx/mailbox` `MailboxStore` over Postgres for +human principals, plus the routes a host's UI uses to list, read, file and +send. It owns the `mailbox` schema, its tables and migrations, the `/me/inbox*` +HTTP surface, MIME frame building and decoding, the durable write path and the +triage mechanism. The host supplies the Hono app, the database handle, the +caller's tenant and principal, the triage vocabulary, sender authorization, +display names, the event bus for multi-replica setups and the mail transport. +This package neither sends nor receives SMTP. + +## Layout + +- `src/mount.ts` — `createMailboxRoutes`, the `/me/inbox*` routes and SSE stream. +- `src/native-store.ts` — the native `MailboxStore` over Postgres (uid and modseq always set). +- `src/write.ts` — the write boundary every host-facing write path goes through. +- `src/persist.ts` — `createMailboxPersist`, the transport dual-write seam. +- `src/frame.ts` — RFC 5322 frame building and decoding. +- `src/recipients.ts` — address-list parsing and owned-mailbox resolution. +- `src/bus.ts` — the event bus contract and the in-memory default. +- `src/purge.ts` — explicit offboarding. +- `src/schema.ts`, `src/schema-check.ts` — drizzle tables and the live-schema check. +- `src/migrations.ts` — `runMailboxMigrations`, replays `migrations/*.sql`. +- `src/db.ts` — the `MailboxDb` handle type. +- `src/index.ts` — the only module consumers import from. +- `e2e/` — real-Postgres suites; shared harness in `e2e/helpers.ts`. + +## Rules + +- Everything from the host arrives through a declared seam, never an import. Wanting to import a host package means adding a port. +- `POST /me/inbox/send` builds the message, files a copy in `Sent`, then calls the host's `deliver` exactly once. +- `createMailboxPersist` calls the host's `upstream` unconditionally and layers the inbox write on top, so a transport failure never costs a recipient their copy, and a mailbox refusal never rejects upstream success. +- `schema.ts` and `migrations/*.sql` change together, in the same commit. Every migration is idempotent. +- Caps refuse rather than clamp: `?limit=` above 200, bulk actions above 50 ids, frames above `MAX_MAILBOX_FRAME_BYTES`, recipients above `MAX_MAILBOX_RECIPIENTS`. +- SSE events are best-effort nudges with a bounded queue (`MAX_PENDING_SSE_EVENTS`); clients refetch on reconnect. The in-memory bus is single-process. +- Nothing is mocked at the database boundary. Unit tests only for load-bearing logic (frame and recipient parsing, guards, migrations); everything else is e2e. +- Every `any` or cast carries a comment saying why the type system leaves no alternative. + +## Local development + +```sh +bun install && bun run check +``` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index a4d41ed..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,531 +0,0 @@ -# Architecture - -How `@corbits/mailbox` is put together, and what it does and does not ask of -the host that mounts it. For install, the mount snippet, the route table and the -response contracts, see the [README](./README.md) — -this document is about structure and reasoning, and does not repeat them. - -## The shape of the thing - -A **library, not a service**. It creates no HTTP server, opens no connection -pool by default, owns no configuration, and starts no background work. A host -calls two functions: - -- `runMailboxMigrations(config, { schema })` — once at boot, before serving, - with the same arguments the host passes Interchange's `runMigrations`. -- `createMailboxRoutes(deps)` — returns a `Hono` sub-app serving - `/me/inbox*`, which the host mounts with `app.route`. - -## Where the routes are served - -The sub-app registers root-relative paths and takes no base path, so the -_mount point_ is the host's decision, the same way Interchange's own -`createGrantRoutes` and friends are mounted: - -```ts -app.route( - "/api/tenants/:tenantId/mailbox", - createMailboxRoutes({ db, bus, requireGrant, senderAddressFor, deliver }), -); -``` - -The host's tenant middleware runs first and sets `tenant` and `principal` on -the context, exactly as it does for its own `TenantEnv` routes. - -## The routes seam - -`createMailboxRoutes(deps: CreateMailboxRoutesDeps): Hono`. -Everything the package cannot know on its own arrives through `deps`; nothing -is reached for. - -| Dep | Required | What it is | -| --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `db` | yes | A drizzle `postgres-js` handle. The schema generic is `any` on purpose, so the host passes the handle it already has instead of opening a second pool. | -| `bus` | yes | `MailboxEventBus` — per-mailbox fan-out backing the SSE route, keyed by the `(tenantId, principalId)` pair (`MailboxEventScope`). `createInMemoryMailboxEventBus()` ships as the zero-config default. | -| `requireGrant` | yes | `@intx/hub-api`'s `RequireGrant`. Reads are gated on `mailbox:*` `read`, send on `create`, flag and move verbs on `manage`. | -| `senderAddressFor` | yes | The caller's `From:` address, from the host's own directory. | -| `deliver` | yes | The host's transport, called once per send after the Sent copy is filed. | -| `heartbeatIntervalMs` | no | SSE keep-alive period, default 25s (under the 30s idle timeout most proxies default to). Exists so a test can observe a heartbeat without waiting. | - -What it does **not** require: no session library, no logger -configuration, no UI. What it _does_ require of the database is an -Interchange-shaped control plane: `tenant` and `principal` in the host schema -of the same database, in place before `runMailboxMigrations` runs, because the -mailbox tables foreign-key to both. Nothing changed in Interchange to make that work — -the coupling lives entirely on this side. - -One further seam lives outside `createMailboxRoutes`, on the write side: -`createMailboxPersist(db, { upstream, authorizeSender, bus?, onRow?, resolveRefs? })` -wraps a host's own mail-persist function so every addressed principal also -gets a durable row. `authorizeSender(address) => { tenantId, domain } | null` -is the host's decision — whether a sender address belongs to a _live_ agent -instance is not a schema fact. Returning `null` skips the mailbox write -entirely while the frame still goes upstream. On the recipient side the -package does consult the control plane: an address whose local part matches -no known principal in the authorized tenant is skipped with a warning rather -than minting a phantom mailbox row, and never costs the frame's real -recipients their durable copy. - -The wrapper's contract is **dual-write independence in both directions**: an -`upstream` throw still attempts the mailbox write and then re-throws the -original error, and a mailbox-write failure is logged and never rejects a -persist that upstream already completed. Under retry, the mailbox side is -idempotent: package-owned transport `messageKey`s plus `onConflictDoNothing` -collapse duplicate frames without failing the call or re-announcing. - -`resolveRefs(args)` — `args` is `MailboxPersistArgs` plus the resolved -`senderAuthorization` and the `decoded` frame (or `null` if the parser -rejected it) — is called ONCE per frame, before the transaction opens, not -once per recipient: a host pointing every row at the same upstream entity -does one lookup, not N. It runs AFTER `upstream` resolves and serially with -it, so its latency adds to the call rather than overlapping. Its result is -validated with `MailboxRefArraySchema` and capped at `MAX_MAILBOX_REFS` the -same way `writeMailboxMessage`'s `refs` argument is (excess entries -truncated from the end of the list, logged with `messageId` and -`senderAddress`, never a throw) — so a resolver must return a small set with -the load-bearing ref FIRST, since anything past the cap is silently dropped. -Refs are then stored on every recipient row of that frame INSIDE the same -transaction — so the post-commit bus `create` event and any SSE subscriber -already see refs on the row once it's readable. - -Refs are frozen at the FIRST successful insert for a given frame: a retry -(same idempotency key) still calls `resolveRefs` — it is not skipped — but -because `onConflictDoNothing` writes no row on a retry, a different result -from that second call is simply discarded; only the first call's refs ever -land. A throwing `resolveRefs` is handled exactly like any other -pre-transaction failure: it falls under the same dual-write contract as the -rest of the wrapper — logged (naming `resolveRefs` as the failing stage), -upstream unaffected (already ran, or still will, independently of this), and -no mailbox row for that frame. - -## Modules - -| File | Role | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `mount.ts` | HTTP surface. Parsing, validation, status codes, SSE. No SQL. | -| `read.ts` | List (cached columns, no `raw`) and detail (frame-decoded) projection, keyset paging, snippets on detail. | -| `thread.ts` | The conversation under one entity ref: keyset-paged oldest-first, parents resolved by RFC 5256 References linking, plus the msg-id lookup. | -| `mutations.ts` | Read/unread, archive, trash, restore, bulk, enrich, assign. | -| `write.ts` | `writeMailboxMessage` / `deliverInboxItems` — the host-facing write API. | -| `persist.ts` | The transport dual-write wrapper and the `authorizeSender` seam. | -| `frame.ts` | Building and decoding RFC 5322 frames, multipart included. | -| `cursor.ts` | Cursor encoding plus the view/sort/filter vocabulary and its fingerprints. | -| `recipients.ts` | Address-list parsing and domain-scoped recipient resolution. | -| `sender-display.ts` | The pure half of display names, plus the resolver seam. | -| `vocabulary.ts` | The host's triage vocabulary: validation, the generated rank, the ordering fingerprint. | -| `schema.ts` / `migrations.ts` | The tables, and the runner that replays `migrations/*.sql`. | -| `bus.ts` / `db.ts` / `refs.ts` | The event-bus port, the db handle type, the ref schema. | - -## Data model - -Two physical tables, both living in a -dedicated `mailbox` Postgres schema in the HOST's database -(`"mailbox"."principal_mail"`, `"mailbox"."mailbox_state"`), never in `public` and -never in a database of their own. Every row in either belongs to exactly one -`(tenant_id, principal_id)` mailbox. - -``` -principal_mail the message as delivered. IMMUTABLE. - id, tenant_id, principal_id, address, direction, raw, - subject, from_address, message_id, in_reply_to, - message_key, refs, created_at - -mailbox the management layer, keyed by mail id. Mutable. - read_at, archived_at, trashed_at (universal) - priority, classification, status, assignee (triage) -``` - -**Why the split.** Interchange's `session_mail` is the message as delivered and -nothing more — no `read_at`, no archive, no triage — because agents don't -triage their inbox. The moment mail is served to a _human_ all of that becomes -necessary, so the management layer is genuinely ours to own; it just does not -belong on the mail row, which has to keep reading 1-1 with Interchange's. -`principal_mail` matches `session_mail` for every column the two share, and -everything a human does to a message afterwards lives one join away. One name -deliberately does _not_ line up: `session_mail.status` is _delivery_ state -while `mailbox.status` is _triage_ state — same word, different meaning, and at -least on different tables. - -**The management row is created eagerly, with its message, in one -transaction** — both on the `writeMailboxMessage` path and on the -`createMailboxPersist` path. An all-NULL row means delivered-and-untouched. -Guaranteed presence is what makes the rest of the design simple: - -- Every mutation is a **plain scoped `UPDATE`** on `mailbox` — no upsert, no - first-touch race. A message outside the caller's scope matches no row, which - the routes read as 404. -- The unread count is an **index-only scan** of the partial index - `mailbox_tenant_id_principal_id_unread_idx` (`WHERE read_at IS NULL AND -archived_at IS NULL AND trashed_at IS NULL`) — possible only because every - message carries a row. -- The single transaction is load-bearing: split, a crash between the two writes - would commit the mail row alone, and a retry would hit the `messageKey` - dedupe and return null, leaving a message no mutation can reach. - -**One foreign key of our own.** `mailbox.id REFERENCES principal_mail(id) -ON DELETE CASCADE` makes a message and its management state one lifecycle. -Each purge (`purgeTenantMailbox`, `purgePrincipalMailbox`) is therefore a -**single `DELETE` on `principal_mail`** — the management rows follow through -the cascade, so a purge is atomic by construction, with no transaction to -manage. Both take the caller's `db` handle, so a host can run them inside its -own offboarding transaction; neither is scoped by view, because an offboarded -tenant's trash is as much their data as their inbox. - -**Hard control-plane foreign keys.** `tenant_id` and `principal_id` on both -tables reference the host schema's `tenant` and `principal`, both -`ON DELETE CASCADE` — the same posture as Interchange's own -`session_mail.tenant_id`, extended to the principal. Consequences, stated -rather than hidden: the control plane and the mail plane must share one -database, the control-plane tables must exist before `runMailboxMigrations` -runs, and there is no separate-database mode. Deleting a tenant or principal -row carries every one of its mailbox rows out; the explicit purges exist for -hosts that soft-delete control-plane rows, where no cascade ever fires. -(`assignee` and `address` remain plain `text` held by value — an assignment -must survive the assignee's principal being offboarded.) - -The **migration DDL is the single owner of the constraints**: the FKs are -declared there and deliberately not restated as drizzle `.references()` thunks -in `schema.ts`, which declares only the columns. The one host table `schema.ts` -still stubs is `principal` (`hostPrincipal`), read by the delivery-time -recipient existence check — never created or migrated here. - -Two layers sit deliberately in front of the FKs: - -- _The write boundary_ (`src/scope.ts`). Every write path refuses a blank or - whitespace-only `tenantId`/`principalId` with a `RangeError` at the boundary, - where the caller still has a stack — the FK would refuse it too, but as a - driver error deep in the insert. `deliverInboxItems` checks the whole batch - before writing any of it, so the refusal is all-or-nothing. Identifiers are - never trimmed on the caller's behalf. -- _The delivery filter_ (`src/persist.ts`). Recipient local parts are - sender-controlled; unknown locals are resolved against `public.principal` - first and skipped with a warning, so one typo'd address never costs the real - recipients on the same frame their durable copy, and external mail cannot - mint a phantom mailbox row. - -**Column types are Interchange's.** Ids are `text` defaulting to -`gen_random_uuid()::text`, not `uuid` — every Interchange table is -`text("id").primaryKey()`, and an id should not change type at the seam. -Timestamps are `timestamp without time zone` holding UTC, and the rule that -follows is one the read path must keep: **the column is never cast**. -`timestamp → timestamptz` is `STABLE`, not `IMMUTABLE`, so a cast on the column -side drops the keyset page out of `Index Cond` into `Filter`. The _cursor_ is -cast instead, and to `::timestamp` — a `timestamptz` literal resolves through -the session's TimeZone, so on a non-UTC host the same cursor silently seeks to -a different row. - -**The raw frame is the authority on detail.** `raw bytea` holds the complete -MIME frame; `subject` and `from_address` are caches parsed once at write time. -List reads those caches only (no `raw`, no decode, no snippet). A frame the MIME -parser rejects still persists — detail reads degrade to an empty body rather -than a 500 — and improving the parser improves _existing_ rows, because nothing -was thrown away at write time. - -**Dedupe is partial on purpose.** The unique index on -`(tenant_id, principal_id, message_key)` is `WHERE message_key IS NOT NULL`. -Mail arriving without a stable key — most external mail — is left -unconstrained rather than collapsed onto a single NULL-keyed row per mailbox. -Keys are namespaced by path: - -- **Inbox ingress** (`mailboxKey.inbox`) uses a versioned length-prefixed - encoding `inbox2:::` so pairs that contain - `:` cannot collide, and so the space is disjoint from pre-upgrade - `inbox::` keys (length-prefix under `inbox:` alone would - false-collide when a historical source was pure decimal). No migration is - performed; redelivery after upgrade may insert a second row. -- **Transport dual-write** stamps `transport:mid::` or - `transport:raw::` and inserts with `onConflictDoNothing`, - so a retried frame does not fail on unique-violation. Management rows and bus - announce only for rows returned by `RETURNING`. -- **Gate / run** keys remain under their own namespaces via `mailboxKey`. - -**Batch delivery is one transaction.** `deliverInboxItems` prevalidates blank -scopes, then commits every new row in the call in a single transaction (or -none). Deduped keys are no-ops inside the transaction. Bus publish and the -optional host `enqueue` hook run only after commit, and only for newly inserted -ids. Both side effects are best-effort: a throw is logged with the message id -and never rejects the delivery. - -**Two write paths, two shapes of batch.** `deliverInboxItems` is the -**notify-item path**: one external item, fanned out to every addressed -principal, keyed by `mailboxKey.inbox(source, externalId)` — unchanged by the -addition below. `writeMailboxMessages(db, items, opts?)` is the -**conversation path**: an arbitrary batch of `{ scope, args }` pairs — a -sender's own outbound copy alongside every recipient's inbound copy of the -same turn, mixed tenants and principals allowed — committed in the same -single-transaction-or-none shape, with per-row `onConflictDoNothing` dedupe on -the same `messageKey` partial unique index and bus events published only -after commit, one per row this call actually inserted. A throw from any one -item (an invalid scope, an oversize frame, a control-plane FK the item's -scope does not satisfy) rolls back every row the batch would otherwise have -written, including ones already inserted earlier in the same call — same -atomicity guarantee as `deliverInboxItems`, over a caller-shaped item instead -of an ingress-shaped one. Each item's `args` is -`Omit` — `scope` is the -sole source of both, so there is no second copy of the scope an item could -disagree with. `writeMailboxMessages` returns one `{ messageKey, id }` entry -per item, in item order — matching `deliverInboxItems`'s `DeliveredInboxItem` -shape — with `id: null` exactly for an item whose messageKey deduped against -an existing row, rather than a filtered array of inserted ids. - -**A write's Message-ID, direction, and dedupe key are now the caller's to -set.** `WriteMailboxMessageArgs.messageId` lets a caller hand the write path -the exact msg-id its own frame must carry (validated as a bracketed msg-id; -`RangeError` otherwise) instead of always minting one — needed when a -message's id has to be predictable ahead of the write, e.g. so a later -`inReplyTo` can reference it. `direction` (default `"inbound"`) is a stored -fact: an outbound row is the sender's own durable copy, and is created -already-read — its `mailbox.read_at` is pinned to its own `created_at` at -insert — so it is excluded from the unread count and the unread view without -either needing a direction predicate of its own. `listUserMailbox` and -`getMailboxMessage` accept an optional `direction?: "inbound" | "outbound" | -"all"` (default `"inbound"`, preserving today's contract) so a thread reader -can fetch a principal's own sent copies or both directions together — see -Known limits. And `messageKey`, when the caller omits it, now defaults to -`mailboxKey.transport(messageId, principalId, direction)`: for the default -`"inbound"` direction this is `transport:mid::` — -the same shape `persist.ts`'s transport dual-write already uses, byte for -byte, so a frame `persist.ts` already delivered and a direct inbound write -for the same Message-ID + principal still dedupe onto the same row — while -`"outbound"` gets a `:outbound` suffix, so a sender's own copy of a turn -never collapses onto an inbound copy that reuses the identical -caller-supplied `messageId` for the same principal. A retry that reuses the -same caller-supplied `messageId` (and direction) therefore dedupes for free -— the write returns `null` — while two writes that each mint their own -`messageId`, or that differ in direction, never collide. A caller-supplied -`messageKey` still overrides the default, exactly as before. - -**Bus publish isolates listeners.** `publishMailboxEvent` invokes each -subscriber independently; one throwing listener does not stop the others. SSE -connections serialize writes, bound the pending queue, and close on overflow or -write failure rather than buffering forever. - -**The event names the operation that fired.** `publishMailboxEvent` takes a -required `op` (`MailboxEventOp`: `create`, `mark_read`, `mark_unread`, `trash`, -`archive`, `restore`, `enrich`, `assign`) and includes it on the published -event. Every call site in this package passes one — the two delivery paths -(`writeMailboxMessage`, `deliverInboxItems`) and the transport dual-write -(`createMailboxPersist`) publish `create`; `createMailboxRoutes`'s route table passes -the mutation's own identifier, reusing `MailboxBulkAction`'s vocabulary for -the single-message verbs so "read one" and "read fifty" report the same op. -`op` stays _optional on `MailboxEventSchema`_ even though it is required to -publish — additive, not a reshape: a listener built against the original -`{ type, id }` shape still validates, and a historical event replayed from -before this field existed still passes. Requiring it on `publishMailboxEvent` -is what keeps every call site _in this package_ honest going forward; it -cannot reach a caller outside the package, which is the other reason the -schema field has to stay optional. - -`MailboxEventOp` deliberately keeps its own name and vocabulary rather than -reusing `MailboxBulkAction`. It is a superset — `create`, `enrich`, and -`assign` are not bulk actions, and never will be — so aliasing the two would -claim an equivalence that does not hold. `mount.ts`'s route table is the one -place that has to know both: it maps HTTP verbs to `MailboxBulkAction` values -that also happen to be valid `MailboxEventOp` values. - -**Triage enriches the message, not a task.** `priority`, `classification`, -`status` and `assignee` are columns on the message's management row, not a -spawned work item. Delegation is the `assignee` ref: the item stays in the -delegator's mailbox. The vocabulary is the host's — `priorities` is ordered, -most urgent first, and that order _is_ the ranking `sort=priority` uses; -`priority` and `status` are plain `text` with no `CHECK`, because a constraint -here would freeze one product's taxonomy into every adopter's database. A value -the host no longer lists — including `NULL` — ranks last. - -**Cursors are bound to the result set that minted them.** A priority cursor -carries a canonical rendering of the host's ordering, and a mismatch is a -`400` — a reordered vocabulary must not silently redefine what an in-flight -rank means. A malformed cursor is always a `400`, never a `500`: the decoded -`createdAt` is pinned to exactly the microsecond `to_char` shape this package -mints, and the priority rank must be a safe integer, so a crafted cursor never -reaches Postgres. - -**Indexes are query-shaped**, named the way Interchange names them, and every -one leads with `(tenant_id, principal_id)` because every query is scoped to one -mailbox. The keyset — `(tenant_id, principal_id, created_at DESC, id DESC)` — -stays on `principal_mail`, matching the list's `ORDER BY` and its row-value -cursor seek exactly, so the default (highest-traffic) page remains a -single-table index scan that stops at `limit + 1` rows. The triage indexes and -the three partial view indexes (`unread`, `archived_at`, `trashed_at`) live on -`mailbox`. The thread read adds three more on `principal_mail`: -`(tenant_id, principal_id, message_id)` — not unique, since a msg-id is the -_sender's_ identifier and nothing stops two delivered frames carrying the same -one — a GIN index on `refs`, the only kind that can serve the `refs @> …` -containment filter the ref scope is expressed as, and -`(tenant_id, principal_id, created_at, id)` matching `readMailboxThread`'s own -oldest-first `ORDER BY` verbatim. That last one covers the same three leading -columns the list path's own keyset index does, in the opposite direction; a -backward scan of the list's index already serves the thread query, but a -dedicated index removes the dependence on the planner choosing to scan the -other one in reverse. Whichever index a page's plan uses, a ref whose messages -cluster at one end of the principal's own `created_at` history while a page -seeks from the other end still costs a `Filter` proportional to how much -_unrelated_ history sits between them — no index shape fixes that; only -clustering by ref would, and this package deliberately holds no opinion on -physical row order. See "What the split costs, measured" below. - -### Thread reads - -`readMailboxThread(db, scope, { ref, cursor?, limit? })` answers the -conversation under one entity ref: oldest first, keyset-paged on -`(created_at, id)`, scoped to `(tenant_id, principal_id)` and filtered by -jsonb containment on `refs`. - -**Parents are resolved by RFC 5256 References linking, never by subject.** For -each message the candidate ancestors are its `In-Reply-To` followed by its -`References` chain walked newest-to-oldest, and the first candidate present in -_this_ mailbox under _this_ ref wins. An ancestor that is not present yields -`parentId: null` — a message whose parent lives in another principal's mailbox, -or under a different ref, is a root of what this reader can see, and inventing -a node for it would be a lie about the conversation. - -**`parentId` chains are acyclic.** RFC 5256 step 1.B calls out that nothing -stops a delivered frame's `In-Reply-To`/`References` from naming a msg-id that, -directly or through further ancestors, points back at the frame itself. Before -a page is projected, the candidate-parent graph is walked (breadth-first, -beyond the page itself when a chain reaches further) and every edge that would -close a loop is cut: the LATER-created message in the cycle (ties broken by -id) becomes a root instead, deterministically — the cut depends only on the -cycle's own membership, never on which page or cursor triggered the read. - -The ancestor lookup spans the whole ref-scoped set rather than the current -page, so a chain crossing a page boundary cannot report a parent on one page -and `null` on another. It costs one query per hop of the ancestry graph — a -msg-id map over the ids referenced so far, served by -`principal_mail_tenant_id_principal_id_message_id_idx` — capped defensively at -`MAX_THREAD_ANCESTRY_NODES` so a pathological reference graph degrades a -resolved parent to `null` rather than reading an unbounded number of rows. - -The whole module runs on the list path and never selects `raw`. That is what -the cached `message_id`, `in_reply_to` and `references` columns exist for: a -thread is read on every conversation open, and decoding one MIME frame per row -would make the cache pointless. `readMailboxMessageByMessageId(db, scope, -messageId)` is the same posture — a scoped lookup on the list projection, -oldest match winning, `null` when this mailbox holds no such message. - -### What the split costs, measured - -`EXPLAIN (ANALYZE, BUFFERS)` on one principal with 60 000 inbound messages -(including a 20 000-row `created_at` tie group) plus 30 000 belonging to -others: - -| Query | Before (one table) | After (split) | -| --------------------------------------------- | ------------------ | ----------------------------------------------- | -| default `created_at` keyset page, deep cursor | 0.08 ms, 9 buffers | 0.36 ms, 172 buffers — same plan shape, no sort | -| `sort=priority` page, deep cursor | 28 ms, 10 332 | **106 ms, 8 031** | -| unread count | index-only scan | index-only scan on `mailbox` | - -The keyset path does not regress in kind — the extra buffers are the -primary-key probes into `mailbox`, one per candidate row. The unread count, -which regressed badly under an earlier lazy-row design, is resolved by eager -row creation: it is once again a single index-only scan of a partial index -that matches its predicate exactly. What remains, honestly: **`sort=priority` -pays a join** over the management layer on top of a rank that was never -index-servable, ~3.8x its pre-split cost. - -`schema.ts` and `migrations/*.sql` must agree statement for statement: the -runtime queries read through the drizzle table object, so a drift between the -two would query columns or rely on indexes the migrations never created. -`e2e/migrations.test.ts` diffs the two against a live database. - -## Migrations - -`runMailboxMigrations(config, { schema })` replays every `migrations/*.sql` -file in filename order on each run, the same way Interchange `runMigrations` -does, and is safe to call unconditionally on every boot of every replica. It -opens one connection from `config` and closes it when done. `schema` names the -host schema holding `tenant` and `principal`; the `"public".` FK references in -the files are rewritten to it. The FKs are fixed by the run that first creates -each table; passing a different `schema` later does not move them. - -- **Every file is idempotent, so there is no ledger.** DDL is `IF NOT EXISTS`, - and every backfill runs inside a `DO` block only in the replay that adds its - column or table, so a replay never rewrites a row. Edit a file only in ways a - database already carrying the old version converges from. -- The whole run is one transaction whose first statements are - `SET LOCAL client_min_messages = warning` and a **transaction-scoped** - advisory lock. `CREATE TABLE IF NOT EXISTS` is not itself race-safe, so the - lock — not the `IF NOT EXISTS` — is what makes concurrent cold starts safe. -- Lowering `client_min_messages` is why a replay prints nothing: postgres.js - would otherwise dump each `IF NOT EXISTS` NOTICE object to the console. -- **Last, on the same transaction, `assertExpectedColumnTypes` runs** - (`src/schema-check.ts`). `CREATE TABLE IF NOT EXISTS` compares the table name - and nothing else, so against a host that already owns a `principal_mail` it - would silently no-op and every read would decode the host's columns through - our codec. The expectation is derived from the drizzle table objects, so it - cannot drift; a rejected boot rolls back everything the run applied. -- 0.1.0 kept a checksum ledger, `"mailbox"."corbits_mailbox_migrations"`. - `0007_drop_migrations_ledger.sql` drops it; a 0.1.0 database upgrades on its - next boot with no manual step and no row changed - (`e2e/upgrade-from-0.1.0.test.ts`). -- A database from before 0.1.0 (run from source) upgrades too: the guarded - backfills in 0002–0004 fill the columns they add from the rows already - there, 0004 takes each message's folder and `\Seen` from the pre-native - `"mailbox"."mailbox"` table, and 0005 then drops that table - (`e2e/upgrade-from-pre-0.1.0.test.ts`). -- The runner rewrites every `"public".` followed by a quoted identifier, so a - migration file may use `"public".` only to qualify a host-table FK. - -**Everything lands in the `mailbox` schema, fully qualified.** Nothing resolves -through `search_path`, so the host's own setting cannot redirect or shadow -where the mailbox tables live. The one ordering constraint mounting imposes is -the control plane's: the DDL's foreign keys reference the host schema's -`tenant` and `principal`, so those tables must exist before the first run. - -## Boundaries - -Owned by this package: the `mailbox` Postgres schema, its two tables, their -indexes and migrations; the `/me/inbox*` HTTP surface, its validation and -status codes; MIME frame construction and decoding; the durable write path and -its idempotency; and the triage _mechanism_ — the ranking, the filters, the -delegation ref. - -Supplied by the host: the Hono app and the database handle (pointed at the -database where `tenant` and `principal` live); the triage **vocabulary**; who -the caller is (the `tenant` and `principal` its tenant middleware sets on the -context); whether a sender may deliver -(`authorizeSender`) and to which tenant; display names -(`resolveSenderDisplays`); an event bus, if one process is not enough; and the -actual mail transport — this package neither sends nor receives SMTP. - -## Known limits - -- **The default bus is single-process.** `createInMemoryMailboxEventBus()` fans - out within one process only. A host running multiple replicas must supply a - broker-backed `MailboxEventBus`, or SSE clients will only see events raised - by the replica they are connected to. -- **SSE events are non-durable nudges.** Publication is best-effort after - commit, each connection's queue is bounded at `MAX_PENDING_SSE_EVENTS` (100), - and a consumer that stops reading is disconnected rather than buffered for. - Events can be missed (dropped publish, overflow disconnect); duplicated, - but only when there is no stable dedupe key to prevent it — an inbox item - redelivered without one, or a broker-backed bus itself redelivering; or - arrive out of order (no cross-replica ordering guarantee). The client - contract — reconnect and refetch the list and unread count on any - disconnect, and never trust event arrival order over a refetch — is - documented in the package README. -- **`sort=priority` pays a cross-table join** on top of a rank that was never - index-servable; see the measurements above. -- **List routes default to inbound rows.** The `direction` column admits - outbound rows and the write path can create them; `listUserMailbox` and - `getMailboxMessage` default to `"inbound"` (preserving the mounted route - table's existing behavior) but accept `direction: "outbound" | "all"` for - a caller — a thread reader, not yet a mounted route — that needs a - principal's own sent copies. There is still no "sent" view or send route - on the mounted API itself. -- **No search.** Filtering is by view, priority, classification, status and - assignee. There is no full-text index over subjects or bodies. -- **Reordering the host's `priorities` invalidates in-flight priority - cursors** — they 400 rather than paging against a ranking that changed - underneath them. Appending a new band has the same effect, because it changes - what the trailing "unknown" rank means. -- **`?limit=` is refused, not clamped,** above 200. A caller that asked for 500 - and silently received 200 would page as though it had 500 rows. -- **Bulk actions cap at 50 ids** and report per-id results; partial success is - the normal outcome, not an error. -- **Frame size is hard-capped at `MAX_MAILBOX_FRAME_BYTES` (1 MiB)** on both - direct write (after `buildMailFrame`) and the transport dual-write path - (raw bytes). **Transport recipient lists hard-cap at - `MAX_MAILBOX_RECIPIENTS` (50)** before resolve / multi-row insert. Both refuse - with `RangeError` rather than clamping; the transport path still preserves - dual-write independence (mailbox refusal does not reject upstream success). diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d3eb515..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,179 +0,0 @@ -# Changelog - -All notable changes to `@corbits/mailbox` are documented here. The format -follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this -package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -Until 1.0, a minor bump may contain a breaking change; breaking changes are -always called out under their own heading. - -## [Unreleased] - -## [0.2.0] — 2026-09-25 - -The package is now an Interchange hub module: a route factory the host mounts, -grant-gated routes, and SQL migrations shaped like Interchange's own. The -exports in `src/index.ts` are the public API as of this release. - -### Breaking - -- `mountMailbox(app, opts)` is gone. Mount `createMailboxRoutes(deps)` with - `app.route`. `MountMailboxOpts` is now `CreateMailboxRoutesDeps`, and - `deps.requireGrant` is required: reads check `mailbox:*` `read`, send checks - `create`, and the flag and move verbs check `manage`. -- The `resolvePrincipal` option is gone. The principal is the `tenant` and - `principal` the host's tenant middleware sets on the context. Every route - returns 403 when the context carries none, including the list routes, which - used to return an empty 200. -- `runMailboxMigrations(db)` is now `runMailboxMigrations(dbConfig, { schema })`, - taking the same arguments as `@intx/db`'s `runMigrations`. `schema` is the - host schema that holds `tenant` and `principal`. `createMailboxDb` is no longer - exported. -- `MigrationChecksumError` is gone. Every `migrations/*.sql` file is idempotent - and replays on each run under an advisory lock, so there is no ledger; - `0007_drop_migrations_ledger.sql` drops 0.1.0's - `"mailbox"."corbits_mailbox_migrations"`. -- The barrel no longer exports the schema objects (`principalMail`, - `mailboxPgSchema`) or the schema-check internals (`expectedColumnTypes`, - `assertExpectedColumnTypes` and others). -- `@intx/db`, `@intx/hub-api` and `hono-openapi` (with its own peers) are now - peer dependencies alongside the other `@intx/*` packages, all at `^0.4.0`. - -### Changed - -- `build` runs `tsc` directly, and the `prepare` install hook is gone. Install - from npm; the published tarball is built at pack time. - -### Upgrading data - -- A 0.1.0 database upgrades on its next boot with no manual step and no row - changed. -- A database from before 0.1.0 upgrades too. Migration 0004 copies each - message's folder (Trash, then Archive, else INBOX) and `\Seen` flag from the - pre-native `"mailbox"."mailbox"` table, and 0005 then runs - `DROP TABLE "mailbox"."mailbox"`. Its triage columns (`priority`, - `classification`, `status`, `assignee`) go with it, as they did in 0.1.0. - -## [0.1.0] — 2026-07-27 - -Initial public release. Nothing has been published before this, so everything is -new; the list below is what the surface consists of rather than what changed. - -- `mountMailbox(app, opts)` — mounts a principal-keyed inbox under `/me/inbox*` - on a host's existing Hono app: list with keyset paging and view/sort/filter, - unread count, an SSE event stream, message detail, the read/unread, archive, - trash and restore mutations, bulk actions, triage enrichment, and delegation - via an `assignee` ref. -- `runMailboxMigrations(db)` — idempotent, advisory-locked, checksum-guarded, - with its own ledger table. Safe to call on every boot of every replica. All - DDL is schema-qualified into a dedicated `mailbox` Postgres schema in the - host's database, and the host's control plane (`public.tenant`, - `public.principal`) must exist before the first run — the foreign keys below - reference it. -- Two tables in the `mailbox` schema, with **hard control-plane foreign keys**: - `tenant_id -> public.tenant(id)` and `principal_id -> public.principal(id)`, - both `ON DELETE CASCADE`, so a row can only belong to a scope the host knows - and offboarding a tenant or principal carries its mailbox rows out with it. - `principal_mail` is the message as delivered and is immutable — it reads 1-1 - with Interchange's `session_mail` for every column the two share. `mailbox` - is the mutable management layer keyed by mail id - (`read_at`/`archived_at`/`trashed_at` plus the triage columns), created - **eagerly with its message in one transaction**: an all-NULL row means - delivered-and-untouched, every mutation is a plain scoped `UPDATE`, and the - unread count is an index-only scan of the partial - `mailbox_tenant_id_principal_id_unread_idx`. The one package-internal foreign - key is `mailbox.id -> principal_mail.id ON DELETE CASCADE`. The raw MIME - frame is stored on the mail row and remains authoritative; `subject` and - `from_address` are caches. - - Interchange's `session_mail` has no read/archive/trash layer at all, because - agents don't triage their inbox. That layer is genuinely ours to own — it just - does not belong on the mail row. - -- **No closed vocabulary anywhere in the package.** `mountMailbox` requires - `vocabulary: { priorities, statuses }` from the host, with no default: - `priorities` is ordered most-urgent-first and the `sort=priority` ranking - `CASE`, the query-string validation and the OpenAPI enums are all generated - from it. `priority` and `status` are plain `text` columns with no `CHECK` and - no drizzle enum. A value the host does not list — including the `NULL` of an - untriaged message — ranks last. `classification` and `assignee` were already - open host-defined strings. There are no `mailboxPriorities`/`mailboxStatuses` - exports and no `MailboxPriority`/`MailboxStatus` types. -- **Priority cursors carry an ordering fingerprint.** The leading component of a - priority keyset is an integer rank read out of the host's list, so a host that - reorders its vocabulary would silently redefine what every in-flight cursor - means. Such a cursor is now refused with a `400`, on the same mechanism and - for the same reason `canonicalMailboxFilter` already refuses a cross-filter - cursor. Date-sorted cursors carry no ranking and are unaffected. -- **Malformed list cursors are always a `400`, never a `500`.** The decoded - `createdAt` is pinned to exactly the microsecond `to_char` shape this package - mints — not merely something JS `new Date()` tolerates — and the priority - rank must be a safe integer, so a crafted cursor never reaches Postgres. -- Index names follow Interchange's convention, `__..._idx`. -- The scope columns are `tenant_id` and `principal_id`, and the corresponding - TypeScript/wire fields are `tenantId` and `principalId` — matching - Interchange's own `session_mail` (`tenant_id`, `direction`, `raw`, - `created_at`) and the sibling `@corbits/*-core` packages, so the two models - read 1-1. Not a breaking change: nothing has been published, and the single - `0001_principal_mailbox` migration was edited in place — for the scope rename - and again for the table split — rather than followed by rename or split - migrations that no deployed database would ever have needed. -- Blank scopes are refused at the write boundary. `writeMailboxMessage`, - `deliverInboxItems`, `enrichMailboxMessage`, `assignMailboxMessage` and the - `createMailboxPersist` seam throw `RangeError` on an empty or whitespace-only - `tenantId`/`principalId`; `deliverInboxItems` validates the whole batch before - writing any of it. The control-plane foreign keys would refuse such a row too, - but only as a driver error deep in the insert — the boundary check fires where - the caller who typed `""` still has a stack to blame. Any other unknown scope - is refused by the database itself. `assertMailboxScope` and - `MailboxScopeIdsSchema` are exported. -- Inbound external mail addressed to a local part that is not a known principal - in the authorized tenant is skipped with a warning rather than minting a - phantom mailbox row — and one typo'd address never costs the frame's real - recipients their durable copy. -- Every write path — `writeMailboxMessage` and the `createMailboxPersist` - wrapper alike — writes the mail row and its management row in **one - transaction**, so a crash between the two can never commit the mail row alone - and strand a delivered message behind the `messageKey` dedupe with no - management row. -- `purgeTenantMailbox(db, tenantId)` and `purgePrincipalMailbox(db, scope)` — - explicit offboarding for hosts that soft-delete or archive control-plane rows, - where the `ON DELETE CASCADE` never fires but the mail data must still go. - Each is a **single `DELETE` on `principal_mail`** — the management rows follow - through the id cascade, so a purge is atomic by construction — and both - return the number of **messages** deleted. Both accept a transaction, so a - host can offboard a tenant atomically with its own work. -- The SSE stream bounds its per-connection queue at `MAX_PENDING_SSE_EVENTS` - (100); on overflow the server closes the stream. Events are non-durable - nudges — the client contract (reconnect and refetch on any disconnect) is in - the package README. -- Host seams: `resolvePrincipal`, a `MailboxEventBus` keyed by the - `(tenantId, principalId)` pair (`MailboxEventScope`) — a principal id is only - unique within its tenant, so a principal-only bus would fan one tenant's - events out to another tenant's same-named principal — with an in-memory - single-process default, an optional batched sender-display resolver, and - `authorizeSender` on the transport dual-write wrapper. -- Write API: `writeMailboxMessage`, `deliverInboxItems`, and - `createMailboxPersist` for wrapping a host's own mail-persist path. -- Requires `@intx/*` 0.2.2 or newer, Node 22+ or Bun 1.1+, Postgres 13+, and an - Interchange-shaped control plane (`public.tenant`, `public.principal`) in the - same database, created before `runMailboxMigrations` runs. - -### Known cost of the split - -Measured with `EXPLAIN (ANALYZE, BUFFERS)` on 60 000 messages in one mailbox -(including a 20 000-row `created_at` tie group) plus 30 000 belonging to others. -The `created_at` keyset path does **not** regress in kind — still a -single-table index scan with an index condition and no sort (0.08 ms -> -0.36 ms, the extra buffers being one primary-key probe per candidate row). The -unread count does not regress at all: eager management rows keep it an -index-only scan of a partial index that matches its predicate exactly. (An -earlier lazy-row design cost it ~26x; anti-join and covering-index alternatives -were measured under that design and rejected before eager rows resolved it.) -What remains: `sort=priority`, 28 ms -> 106 ms — it was already a sequential -scan plus a top-N sort, the rank was never index-servable, and is now that plus -a join over the management layer. - -[Unreleased]: https://github.com/corbitsdev/corbits-mailbox/compare/v0.2.0...HEAD -[0.2.0]: https://github.com/corbitsdev/corbits-mailbox/compare/v0.1.0...v0.2.0 -[0.1.0]: https://github.com/corbitsdev/corbits-mailbox/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfea6f3..e777312 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,103 +1,21 @@ # Contributing -A small, deliberately boring codebase: strict TypeScript, arktype at the boundaries, -drizzle for data access, no magic. +## Development -## Setup - -```bash -git clone https://github.com/corbitsdev/corbits-mailbox.git -cd corbits-mailbox +```sh bun install -docker run -d --name mailbox-pg -p 5433:5432 \ - -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=mailbox_core postgres:16 - -bun run typecheck -bun run test -bun run test:e2e -bun run build +bun run check ``` -Some end-to-end suites in `e2e/` create and drop a database each, so the test role -needs `CREATEDB`. - -## How it works - -Writes go through a native `MailboxStore` (uid and modseq always set). Search and -threads are `@intx/mailbox` run over that store. `POST /me/inbox/send` builds the -RFC 5322 message, files a copy in `Sent`, then calls the host's `deliver` exactly once. -See [ARCHITECTURE.md](./ARCHITECTURE.md). - -## Code - -`bun run typecheck` must be clean — it is its own CI step, and `any` is not a way past it. The -few escapes in the tree each carry a comment explaining why the type system leaves no -alternative; new ones need the same. - -## Most of the suite needs a real Postgres - -Nothing is mocked at the database boundary. Migrations, indexes, cursors and -concurrency are asserted against a live server, because that is the only place they are -true. Database-touching tests clean up after themselves and must not assume they are -alone — concurrency behavior is part of the contract here. - -The suite connects to `MAILBOX_TEST_DATABASE_URL`, which defaults to -`postgres://postgres:postgres@localhost:5433/mailbox_core` (the CI service's port). - -## Acceptance scenarios live in corbitsdev/examples - -The end-to-end acceptance scenarios that mount this package on a real -`@intx/hub-api` app against a live Postgres live in the `corbitsdev/examples` -repository, not here. If you change the mount seam, the write path, or anything -about how a host wires this up, show that change working there. - -## Dependencies +`bun run check` runs typecheck, lint, format check and unit tests. `bun run format` rewrites the tree. -Everything this package needs from a host arrives through a declared seam, never -through an import. Wanting to import a host-side package is the signal to add a -port instead. +Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how. -## Tests - -- **Unit tests are for load-bearing logic only** — the frame parser, the recipient - parser, the scope and size guards, and migration idempotency and backfills. They live - next to the code as `src/.test.ts` and never ship. -- **Everything else is end-to-end** in `e2e/`, against a real Postgres and the mounted - routes; shared harness code lives in `e2e/helpers.ts`. -- **Red first.** A bug fix starts with a test that fails for the reason you believe, and - you should watch it fail. A test that was green before the fix proved nothing. -- Assert **behavior a consumer can observe** — a status code, a returned shape, a row in - the database — over internal call shapes. +Most suites need a real Postgres: the unit tests for migrations and the write path, and everything in `e2e/`. They connect to `MAILBOX_TEST_DATABASE_URL`, which defaults to `postgres://postgres:postgres@localhost:5432/mailbox_core`. Start one with `docker run -d --name mailbox-pg -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=mailbox_core postgres:16`, then run `bun run test:e2e`. Some suites create and drop a database each, so the role needs `CREATEDB`. ## Migrations -Every file in `migrations/` replays on each run, so each must be idempotent: DDL is -`IF NOT EXISTS`, and a backfill runs in a `DO` block only in the replay that adds its -column or table. Add a new file for a schema change; `e2e/upgrade-from-0.1.0.test.ts` -proves a 0.1.0 database upgrades with no row changed and replays as a no-op. - -`schema.ts` and `migrations/*.sql` must agree statement for statement — the runtime -queries read through the drizzle table object, and `e2e/migrations.test.ts` -diffs the two against a live database. Change one, change the other, in the same commit. - -## Agent-originated mail - -`createMailboxPersist` wraps the host's own persist function. `authorizeSender` is the -host's check that a sender address is one it recognizes right now: a hub answers by -looking up the tenant a mailbox-routable address (a person, or a live agent run) -currently resolves to, and refuses anything else. `upstream` is the host's existing -mail-persist path. The wrapper calls it unconditionally and layers the durable inbox -write on top, so a transport failure never costs a recipient the copy that makes the -message readable later. The host calls the wrapped function wherever it delegates an -outbound frame; it does both writes. - -## Pull requests - -- Keep commits focused, and keep the diff to the change you are describing. -- Explain _why_ in the commit message; the code already says what. -- CI must be green: typecheck, unit, e2e, build, - and a Node consumer smoke test that installs the packed tarball. -- Contributions are accepted under the repository's LGPL-2.1-only licence. +Every file in `migrations/` replays on each run, so each must be idempotent: DDL is `IF NOT EXISTS`, and a backfill runs in a `DO` block only in the replay that adds its column or table. Add a new file for a schema change; `e2e/upgrade-from-0.1.0.test.ts` proves a 0.1.0 database upgrades with no row changed and replays as a no-op. `schema.ts` and `migrations/*.sql` must agree statement for statement; `e2e/migrations.test.ts` diffs the two against a live database. ## Commit messages @@ -105,3 +23,16 @@ Commit subjects and PR titles follow [Conventional Commits](https://www.conventi Add `!` only for public API breaks: removed or renamed exports, changed signatures, newly required params. Peer and dependency range changes are `build(deps):` with no `!`. Keep subjects imperative, lowercase after the colon, 72 characters or less, and free of ticket IDs. Every PR links its issue with a `Closes ` line in the PR body. + +## Releasing + +Releases are manual. On a clean, up-to-date `main`: + +```sh +npm version -m "chore(release): %s" +git push --follow-tags +gh release create "v$(node -p 'require("./package.json").version')" --generate-notes +npm publish +``` + +Bump minor only for breaking API changes; everything else is a patch. `prepack` builds `dist/` from the tagged commit. diff --git a/README.md b/README.md index f59a5cc..1717f87 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # @corbits/mailbox -[![npm](https://img.shields.io/npm/v/@corbits/mailbox.svg)](https://www.npmjs.com/package/@corbits/mailbox) [![License: LGPL-2.1](https://img.shields.io/badge/license-LGPL--2.1-green.svg)](https://github.com/corbitsdev/corbits-mailbox/blob/main/LICENSE) - A Corbits hub module that gives human principals in an Interchange hub an IMAP-style mailbox, mounted as Hono routes on `@intx/hub-api` and stored in the hub's Postgres. The hub is Interchange's multi-tenant control plane; a principal is an account with its own identity and permissions, and a grant is a permission a principal holds on a resource. The routes list, thread, flag, move and send mail, each checked against the caller's grants, with live updates over SSE. ## Why @corbits/mailbox?