From 0a763d2ec535b1d1b3bbc582e5cb9e82327a8709 Mon Sep 17 00:00:00 2001 From: nvms Date: Tue, 27 Aug 2024 18:16:34 -0400 Subject: [PATCH] relocate these --- .gitignore | 7 + .prettierrc | 23 + LICENSE.md | 73 + README.md | 13 + packages/arc | 1 + packages/duplex/.npmignore | 2 + packages/duplex/README.md | 82 + packages/duplex/bump.config.ts | 7 + packages/duplex/bun.lockb | Bin 0 -> 77157 bytes packages/duplex/package.json | 27 + packages/duplex/src/client/commandclient.ts | 230 +++ packages/duplex/src/client/queue.ts | 51 + packages/duplex/src/common/codeerror.ts | 15 + packages/duplex/src/common/command.ts | 25 + packages/duplex/src/common/connection.ts | 70 + packages/duplex/src/common/errorserializer.ts | 37 + packages/duplex/src/common/message.ts | 65 + packages/duplex/src/common/status.ts | 6 + packages/duplex/src/example/client.ts | 27 + packages/duplex/src/example/server.ts | 18 + packages/duplex/src/index.ts | 5 + packages/duplex/src/server/commandserver.ts | 179 ++ packages/duplex/src/server/ids.ts | 40 + packages/duplex/tsconfig.json | 11 + packages/duplex/tsup.config.ts | 11 + packages/express-keepalive-ws/.npmignore | 2 + packages/express-keepalive-ws/README.md | 43 + packages/express-keepalive-ws/bump.config.ts | 7 + packages/express-keepalive-ws/bun.lockb | Bin 0 -> 78303 bytes packages/express-keepalive-ws/package.json | 30 + packages/express-keepalive-ws/src/index.ts | 65 + packages/express-keepalive-ws/tsconfig.json | 11 + packages/express-keepalive-ws/tsup.config.ts | 11 + packages/express-session-auth/.npmignore | 2 + packages/express-session-auth/README.md | 86 + packages/express-session-auth/bump.config.ts | 7 + packages/express-session-auth/bun.lockb | Bin 0 -> 122206 bytes .../express-session-auth.d.ts | 10 + packages/express-session-auth/package.json | 46 + packages/express-session-auth/src/errors.ts | 118 ++ packages/express-session-auth/src/index.ts | 5 + .../express-session-auth/src/middleware.ts | 1489 +++++++++++++++++ .../src/user-confirmation.entity.ts | 32 + .../src/user-remember.entity.ts | 26 + .../src/user-reset.entity.ts | 26 + .../express-session-auth/src/user.entity.ts | 106 ++ packages/express-session-auth/src/util.ts | 68 + packages/express-session-auth/tsconfig.json | 11 + packages/express-session-auth/tsup.config.ts | 11 + packages/hash/.npmignore | 2 + packages/hash/README.md | 43 + packages/hash/bump.config.ts | 7 + packages/hash/bun.lockb | Bin 0 -> 77155 bytes packages/hash/package.json | 27 + packages/hash/src/index.ts | 58 + packages/hash/tsconfig.json | 11 + packages/hash/tsup.config.ts | 11 + packages/ids/.npmignore | 2 + packages/ids/README.md | 30 + packages/ids/bump.config.ts | 7 + packages/ids/bun.lockb | Bin 0 -> 92409 bytes packages/ids/package.json | 30 + packages/ids/src/index.ts | 74 + packages/ids/tests/index.ts | 36 + packages/ids/tsconfig.json | 11 + packages/ids/tsup.config.ts | 11 + packages/jwt/.npmignore | 2 + packages/jwt/README.md | 43 + packages/jwt/bump.config.ts | 7 + packages/jwt/bun.lockb | Bin 0 -> 77893 bytes packages/jwt/package.json | 31 + packages/jwt/src/index.ts | 314 ++++ packages/jwt/tsconfig.json | 11 + packages/jwt/tsup.config.ts | 11 + packages/keepalive-ws/.npmignore | 2 + packages/keepalive-ws/README.md | 98 ++ packages/keepalive-ws/bump.config.ts | 7 + packages/keepalive-ws/bun.lockb | Bin 0 -> 77925 bytes packages/keepalive-ws/package.json | 49 + packages/keepalive-ws/src/client/client.ts | 157 ++ .../keepalive-ws/src/client/connection.ts | 198 +++ packages/keepalive-ws/src/client/ids.ts | 44 + packages/keepalive-ws/src/client/index.ts | 2 + packages/keepalive-ws/src/client/queue.ts | 50 + packages/keepalive-ws/src/index.ts | 2 + packages/keepalive-ws/src/server/command.ts | 19 + .../keepalive-ws/src/server/connection.ts | 88 + packages/keepalive-ws/src/server/index.ts | 294 ++++ packages/keepalive-ws/src/server/latency.ts | 15 + packages/keepalive-ws/src/server/ping.ts | 3 + packages/keepalive-ws/tsconfig.json | 18 + packages/ms/.npmignore | 2 + packages/ms/README.md | 33 + packages/ms/bump.config.ts | 7 + packages/ms/bun.lockb | Bin 0 -> 76713 bytes packages/ms/package.json | 26 + packages/ms/src/index.ts | 174 ++ packages/ms/tsconfig.json | 11 + packages/ms/tsup.config.ts | 11 + packages/otp/.npmignore | 2 + packages/otp/README.md | 88 + packages/otp/bump.config.ts | 7 + packages/otp/bun.lockb | Bin 0 -> 106228 bytes packages/otp/package.json | 33 + packages/otp/src/code.ts | 34 + packages/otp/src/index.test.ts | 246 +++ packages/otp/src/index.ts | 186 ++ packages/otp/src/validate.ts | 8 + packages/otp/tsconfig.json | 11 + packages/otp/tsup.config.ts | 11 + 110 files changed, 5952 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 LICENSE.md create mode 100644 README.md create mode 160000 packages/arc create mode 100644 packages/duplex/.npmignore create mode 100644 packages/duplex/README.md create mode 100644 packages/duplex/bump.config.ts create mode 100755 packages/duplex/bun.lockb create mode 100644 packages/duplex/package.json create mode 100644 packages/duplex/src/client/commandclient.ts create mode 100644 packages/duplex/src/client/queue.ts create mode 100644 packages/duplex/src/common/codeerror.ts create mode 100644 packages/duplex/src/common/command.ts create mode 100644 packages/duplex/src/common/connection.ts create mode 100644 packages/duplex/src/common/errorserializer.ts create mode 100644 packages/duplex/src/common/message.ts create mode 100644 packages/duplex/src/common/status.ts create mode 100644 packages/duplex/src/example/client.ts create mode 100644 packages/duplex/src/example/server.ts create mode 100644 packages/duplex/src/index.ts create mode 100644 packages/duplex/src/server/commandserver.ts create mode 100644 packages/duplex/src/server/ids.ts create mode 100644 packages/duplex/tsconfig.json create mode 100644 packages/duplex/tsup.config.ts create mode 100644 packages/express-keepalive-ws/.npmignore create mode 100644 packages/express-keepalive-ws/README.md create mode 100644 packages/express-keepalive-ws/bump.config.ts create mode 100755 packages/express-keepalive-ws/bun.lockb create mode 100644 packages/express-keepalive-ws/package.json create mode 100644 packages/express-keepalive-ws/src/index.ts create mode 100644 packages/express-keepalive-ws/tsconfig.json create mode 100644 packages/express-keepalive-ws/tsup.config.ts create mode 100644 packages/express-session-auth/.npmignore create mode 100644 packages/express-session-auth/README.md create mode 100644 packages/express-session-auth/bump.config.ts create mode 100755 packages/express-session-auth/bun.lockb create mode 100644 packages/express-session-auth/express-session-auth.d.ts create mode 100644 packages/express-session-auth/package.json create mode 100644 packages/express-session-auth/src/errors.ts create mode 100644 packages/express-session-auth/src/index.ts create mode 100644 packages/express-session-auth/src/middleware.ts create mode 100644 packages/express-session-auth/src/user-confirmation.entity.ts create mode 100644 packages/express-session-auth/src/user-remember.entity.ts create mode 100644 packages/express-session-auth/src/user-reset.entity.ts create mode 100644 packages/express-session-auth/src/user.entity.ts create mode 100644 packages/express-session-auth/src/util.ts create mode 100644 packages/express-session-auth/tsconfig.json create mode 100644 packages/express-session-auth/tsup.config.ts create mode 100644 packages/hash/.npmignore create mode 100644 packages/hash/README.md create mode 100644 packages/hash/bump.config.ts create mode 100755 packages/hash/bun.lockb create mode 100644 packages/hash/package.json create mode 100644 packages/hash/src/index.ts create mode 100644 packages/hash/tsconfig.json create mode 100644 packages/hash/tsup.config.ts create mode 100644 packages/ids/.npmignore create mode 100644 packages/ids/README.md create mode 100644 packages/ids/bump.config.ts create mode 100755 packages/ids/bun.lockb create mode 100644 packages/ids/package.json create mode 100644 packages/ids/src/index.ts create mode 100644 packages/ids/tests/index.ts create mode 100644 packages/ids/tsconfig.json create mode 100644 packages/ids/tsup.config.ts create mode 100644 packages/jwt/.npmignore create mode 100644 packages/jwt/README.md create mode 100644 packages/jwt/bump.config.ts create mode 100755 packages/jwt/bun.lockb create mode 100644 packages/jwt/package.json create mode 100644 packages/jwt/src/index.ts create mode 100644 packages/jwt/tsconfig.json create mode 100644 packages/jwt/tsup.config.ts create mode 100644 packages/keepalive-ws/.npmignore create mode 100644 packages/keepalive-ws/README.md create mode 100644 packages/keepalive-ws/bump.config.ts create mode 100755 packages/keepalive-ws/bun.lockb create mode 100644 packages/keepalive-ws/package.json create mode 100644 packages/keepalive-ws/src/client/client.ts create mode 100644 packages/keepalive-ws/src/client/connection.ts create mode 100644 packages/keepalive-ws/src/client/ids.ts create mode 100644 packages/keepalive-ws/src/client/index.ts create mode 100644 packages/keepalive-ws/src/client/queue.ts create mode 100644 packages/keepalive-ws/src/index.ts create mode 100644 packages/keepalive-ws/src/server/command.ts create mode 100644 packages/keepalive-ws/src/server/connection.ts create mode 100644 packages/keepalive-ws/src/server/index.ts create mode 100644 packages/keepalive-ws/src/server/latency.ts create mode 100644 packages/keepalive-ws/src/server/ping.ts create mode 100644 packages/keepalive-ws/tsconfig.json create mode 100644 packages/ms/.npmignore create mode 100644 packages/ms/README.md create mode 100644 packages/ms/bump.config.ts create mode 100755 packages/ms/bun.lockb create mode 100644 packages/ms/package.json create mode 100644 packages/ms/src/index.ts create mode 100644 packages/ms/tsconfig.json create mode 100644 packages/ms/tsup.config.ts create mode 100644 packages/otp/.npmignore create mode 100644 packages/otp/README.md create mode 100644 packages/otp/bump.config.ts create mode 100755 packages/otp/bun.lockb create mode 100644 packages/otp/package.json create mode 100644 packages/otp/src/code.ts create mode 100644 packages/otp/src/index.test.ts create mode 100644 packages/otp/src/index.ts create mode 100644 packages/otp/src/validate.ts create mode 100644 packages/otp/tsconfig.json create mode 100644 packages/otp/tsup.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b559c91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +^.env$ + +packages/smol +docs +examples diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..73849d9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,23 @@ +{ + "printWidth": 80, + "overrides": [ + { + "files": "packages/otp/src/index.test.ts", + "options": { + "printWidth": 250 + } + }, + { + "files": "packages/otp/src/index.ts", + "options": { + "printWidth": 250 + } + }, + { + "files": "packages/match/src/index.ts", + "options": { + "printWidth": 250 + } + } + ] +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..cd4f67f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,73 @@ +Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2024 Jonathan Pyers + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1aef7e7 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# prsm + +The **prsm** package namespace contains a collection of packages that have been curated over the years by [nvms](https://github.com/nvms). + +* @prsm/arc [![NPM version](https://img.shields.io/npm/v/@prsm/arc?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/arc) +* @prsm/duplex [![NPM version](https://img.shields.io/npm/v/@prsm/duplex?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/duplex) +* @prsm/express-keepalive-ws [![NPM version](https://img.shields.io/npm/v/@prsm/express-keepalive-ws?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/express-keepalive-ws) +* @prsm/express-session-auth [![NPM version](https://img.shields.io/npm/v/@prsm/express-session-auth?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/express-session-auth) +* @prsm/hash [![NPM version](https://img.shields.io/npm/v/@prsm/hash?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/hash) +* @prsm/ids [![NPM version](https://img.shields.io/npm/v/@prsm/ids?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/ids) +* @prsm/jwt [![NPM version](https://img.shields.io/npm/v/@prsm/jwt?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/jwt) +* @prsm/keepalive-ws [![NPM version](https://img.shields.io/npm/v/@prsm/keepalive-ws?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/keepalive-ws) +* @prsm/ms [![NPM version](https://img.shields.io/npm/v/@prsm/ms?color=0B3437&label=)](https://www.npmjs.com/package/@prsm/ms) diff --git a/packages/arc b/packages/arc new file mode 160000 index 0000000..595e0b4 --- /dev/null +++ b/packages/arc @@ -0,0 +1 @@ +Subproject commit 595e0b41961a6504b957173fa6470e4e21d16296 diff --git a/packages/duplex/.npmignore b/packages/duplex/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/duplex/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/duplex/README.md b/packages/duplex/README.md new file mode 100644 index 0000000..a31e68c --- /dev/null +++ b/packages/duplex/README.md @@ -0,0 +1,82 @@ +# duplex + +[![NPM version](https://img.shields.io/npm/v/@prsm/duplex?color=a1b858&label=)](https://www.npmjs.com/package/@prsm/duplex) + +An optionally-secure, full-duplex TCP command server and client on top of `node:tls` and `node:net`. + +## Server + +```typescript +import { CommandServer } from "@prsm/duplex"; + +// An insecure CommandServer (`Server` from `node:net`) +const server = new CommandServer({ + host: "localhost", + port: 3351, + secure: false, +}); + +// A secure CommandServer (`Server` from `node:tls`) +// https://nodejs.org/api/tls.html#new-tlstlssocketsocket-options +const server = new CommandServer({ + host: "localhost", + port: 3351, + secure: true, + key: fs.readFileSync("certs/server/server.key"), + cert: fs.readFileSync("certs/server/server.crt"), + ca: fs.readFileSync("certs/server/ca.crt"), + requestCert: true, +}); + +// ------------------- +// Defining a command handler +server.command(0, async (payload: any, connection: Connection) => { + return { ok: "OK" }; +}); +``` + +## Client + +```typescript +import { CommandClient } from "@prsm/duplex"; + +// An insecure client (`Socket` from `node:net`) +const client = new CommandClient({ + host: "localhost", + port: 3351, + secure: false, +}); + +// A secure client (`TLSSocket` from `node:tls`) +const client = new CommandClient({ + host: "localhost", + port: 3351, + secure: true, + key: fs.readFileSync("certs/client/client.key"), + cert: fs.readFileSync("certs/client/client.crt"), + ca: fs.readFileSync("certs/ca/ca.crt"), +}); + +// ------------------- +// Awaiting the response +try { + const response = await client.command(0, { some: "payload" }, 1000); + // command^ ^payload ^expiration + // response: { ok: "OK" }; +} catch (error) { + console.error(error); +} + +// ...or receiving the response in a callback +const callback = (response: any, error: CodeError) => { + if (error) { + console.error(error.code); + return; + } + + // response is { ok: "OK" } +}; + +// Sending a command to the server +client.command(0, { some: "payload" }, 1000, callback); +``` diff --git a/packages/duplex/bump.config.ts b/packages/duplex/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/duplex/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/duplex/bun.lockb b/packages/duplex/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e080ae2adc94290c657ed240bf69ee0de52368d1 GIT binary patch literal 77157 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 N%_iHi{4f0Pe*n{dVGaNQ literal 0 HcmV?d00001 diff --git a/packages/duplex/package.json b/packages/duplex/package.json new file mode 100644 index 0000000..755fc52 --- /dev/null +++ b/packages/duplex/package.json @@ -0,0 +1,27 @@ +{ + "name": "@prsm/duplex", + "version": "1.1.12", + "author": "nvms", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "description": "", + "keywords": [], + "license": "Apache-2.0", + "scripts": { + "build": "tsup", + "release": "bumpp package.json && npm publish --access public" + }, + "type": "module", + "devDependencies": { + "@types/node": "^22.4.1", + "bumpp": "^9.5.1", + "tsup": "^8.2.4", + "typescript": "^5.5.4" + } +} diff --git a/packages/duplex/src/client/commandclient.ts b/packages/duplex/src/client/commandclient.ts new file mode 100644 index 0000000..be385de --- /dev/null +++ b/packages/duplex/src/client/commandclient.ts @@ -0,0 +1,230 @@ +import { EventEmitter } from "node:events"; +import net from "node:net"; +import tls from "node:tls"; +import { CodeError } from "../common/codeerror"; +import { Command } from "../common/command"; +import { Connection } from "../common/connection"; +import { ErrorSerializer } from "../common/errorserializer"; +import { Status } from "../common/status"; +import { IdManager } from "../server/ids"; +import { Queue } from "./queue"; + +export type TokenClientOptions = tls.ConnectionOptions & net.NetConnectOpts & { + secure: boolean; +}; + +class TokenClient extends EventEmitter { + public options: TokenClientOptions; + private socket: tls.TLSSocket | net.Socket; + private connection: Connection | null = null; + private hadError: boolean; + status: Status; + + constructor(options: TokenClientOptions) { + super(); + this.options = options; + this.connect(); + } + + connect(callback?: () => void) { + if (this.status >= Status.CLOSED) { + return false; + } + + this.hadError = false; + this.status = Status.CONNECTING; + + if (this.options.secure) { + this.socket = tls.connect(this.options, callback); + } else { + this.socket = net.connect(this.options, callback); + } + + this.connection = null; + this.applyListeners(); + + return true; + } + + close(callback?: () => void) { + if (this.status <= Status.CLOSED) return false; + + this.status = Status.CLOSED; + this.socket.end(() => { + this.connection = null; + if (callback) callback(); + }); + + return true; + } + + send(buffer: Buffer) { + if (this.connection) { + return this.connection.send(buffer); + } + + return false; + } + + private applyListeners() { + this.socket.on("error", (error) => { + this.hadError = true; + this.emit("error", error); + }); + + this.socket.on("close", () => { + this.status = Status.OFFLINE; + this.emit("close", this.hadError); + }); + + this.socket.on("secureConnect", () => { + this.updateConnection(); + this.status = Status.ONLINE; + this.emit("connect"); + }); + + this.socket.on("connect", () => { + this.updateConnection(); + this.status = Status.ONLINE; + this.emit("connect"); + }); + } + + private updateConnection() { + const connection = new Connection(this.socket); + + connection.on("token", (token) => { + this.emit("token", token, connection); + }); + + connection.on("remoteClose", () => { + this.emit("remoteClose", connection); + }); + + this.connection = connection; + } +} + +class QueueClient extends TokenClient { + private queue = new Queue(); + + constructor(options: TokenClientOptions) { + super(options); + this.applyEvents(); + } + + sendBuffer(buffer: Buffer, expiresIn: number) { + const success = this.send(buffer); + + if (!success) { + this.queue.add(buffer, expiresIn); + } + } + + private applyEvents() { + this.on("connect", () => { + while (!this.queue.isEmpty) { + const item = this.queue.pop(); + this.sendBuffer(item.value, item.expiresIn); + } + }); + } + + close() { + return super.close(); + } +} + +export class CommandClient extends QueueClient { + private ids = new IdManager(0xFFFF); + private callbacks: { + [id: number]: (error: Error | null, result?: any) => void + } = {}; + + constructor(options: TokenClientOptions) { + super(options); + this.init(); + } + + private init() { + this.on("token", (buffer: Buffer) => { + try { + const data = Command.parse(buffer); + + if (this.callbacks[data.id]) { + if (data.command === 255) { + const error = ErrorSerializer.deserialize(data.payload); + this.callbacks[data.id](error, undefined); + } else { + this.callbacks[data.id](null, data.payload); + } + } + } catch (error) { + this.emit("error", error); + } + }); + } + + async command(command: number, payload: any, expiresIn: number = 30_000, callback: (result: any, error: CodeError | Error | null) => void | undefined = undefined) { + if (command === 255) { + throw new CodeError("Command 255 is reserved.", "ERESERVED", "CommandError"); + } + + const id = this.ids.reserve(); + const buffer = Command.toBuffer({ id, command, payload }) + + this.sendBuffer(buffer, expiresIn); + + // No 0, null or Infinity. + // Fallback to a reasonable default. + if (expiresIn === 0 || expiresIn === null || expiresIn === Infinity) { + expiresIn = 60_000; + } + + const response = this.createResponsePromise(id); + const timeout = this.createTimeoutPromise(id, expiresIn); + + if (typeof callback === "function") { + try { + const ret = await Promise.race([response, timeout]); + + try { + callback(ret, undefined); + } catch (callbackError) { /* */ } + } catch (error) { + callback(undefined, error); + } + } else { + return Promise.race([response, timeout]); + } + } + + private createTimeoutPromise(id: number, expiresIn: number) { + return new Promise((resolve, reject) => { + setTimeout(() => { + this.ids.release(id); + delete this.callbacks[id]; + reject(new CodeError("Command timed out.", "ETIMEOUT", "CommandError")); + }, expiresIn); + }); + } + + private createResponsePromise(id: number) { + return new Promise((resolve, reject) => { + this.callbacks[id] = (error: Error | null, result?: any) => { + this.ids.release(id); + delete this.callbacks[id]; + + if (error) { + reject(error); + } else { + resolve(result); + } + } + }); + } + + close() { + return super.close(); + } +} diff --git a/packages/duplex/src/client/queue.ts b/packages/duplex/src/client/queue.ts new file mode 100644 index 0000000..a95c419 --- /dev/null +++ b/packages/duplex/src/client/queue.ts @@ -0,0 +1,51 @@ +export class QueueItem { + value: T; + private expiration: number; + + constructor(value: T, expiresIn: number) { + this.value = value; + this.expiration = Date.now() + expiresIn; + } + + get expiresIn() { + return this.expiration - Date.now(); + } + + get isExpired() { + return Date.now() > this.expiration; + } +} + +export class Queue { + private items: QueueItem[] = []; + + add(item: T, expiresIn: number) { + this.items.push(new QueueItem(item, expiresIn)); + } + + get isEmpty() { + let i = this.items.length; + + while (i--) { + if (this.items[i].isExpired) { + this.items.splice(i, 1); + } else { + return false; + } + } + + return true; + } + + pop(): QueueItem | null { + while (this.items.length) { + const item = this.items.shift(); + + if (!item.isExpired) { + return item; + } + } + + return null; + } +} diff --git a/packages/duplex/src/common/codeerror.ts b/packages/duplex/src/common/codeerror.ts new file mode 100644 index 0000000..ac0e062 --- /dev/null +++ b/packages/duplex/src/common/codeerror.ts @@ -0,0 +1,15 @@ +export class CodeError extends Error { + code: string; + name: string; + + constructor(message: string, code?: string, name?: string) { + super(message); + if (typeof code === "string") { + this.code = code; + } + if (typeof name === "string") { + this.name = name; + } + } +} + diff --git a/packages/duplex/src/common/command.ts b/packages/duplex/src/common/command.ts new file mode 100644 index 0000000..91b6162 --- /dev/null +++ b/packages/duplex/src/common/command.ts @@ -0,0 +1,25 @@ +interface CommandData { + id: number; + command: number; + payload: any; +} + +export class Command { + static toBuffer({ payload, id, command }: CommandData): Buffer { + if (payload === undefined) throw new TypeError("The payload must not be undefined!"); + const payloadString = JSON.stringify(payload); + const buffer = Buffer.allocUnsafe(payloadString.length + 3); + buffer.writeUInt16LE(id, 0); + buffer.writeUInt8(command, 2); + buffer.write(payloadString, 3); + return buffer; + } + + static parse(buffer: Buffer): CommandData { + if (buffer.length < 3) throw new TypeError(`Token too short! Expected at least 3 bytes, got ${buffer.length}!`); + const id = buffer.readUInt16LE(0); + const command = buffer.readUInt8(2); + const payload = JSON.parse(buffer.toString("utf8", 3)); + return { id, command, payload }; + } +} diff --git a/packages/duplex/src/common/connection.ts b/packages/duplex/src/common/connection.ts new file mode 100644 index 0000000..c698710 --- /dev/null +++ b/packages/duplex/src/common/connection.ts @@ -0,0 +1,70 @@ +import { EventEmitter } from "node:events"; +import { Duplex } from "node:stream"; +import { Message, NEWLINE } from "./message"; + +const CLOSE_TOKEN = Buffer.from("\\\n"); + +export class Connection extends EventEmitter { + private readonly duplex: Duplex; + private buffer = Buffer.allocUnsafe(0); + + constructor(duplex: Duplex) { + super(); + this.duplex = duplex; + this.applyListeners(); + } + + private applyListeners() { + this.duplex.on("data", (buffer: Buffer) => { + this.buffer = Buffer.concat([this.buffer, buffer]); + this.parse(); + }); + + this.duplex.on("close", () => { + this.emit("close"); + }); + } + + private parse() { + while (this.buffer.length > 0) { + const i = this.buffer.indexOf(NEWLINE); + + if (i === -1) break; + + // +1 to include the separating newline. + const data = this.buffer.subarray(0, i + 1); + + + if (data.equals(CLOSE_TOKEN)) { + this.emit("remoteClose"); + } else { + this.emit("token", Message.unescape(data)); + } + + this.buffer = this.buffer.subarray(i + 1); + } + } + + get isDead() { + return !this.duplex.writable || !this.duplex.readable; + } + + send(buffer: Buffer) { + if (this.isDead) return false; + + this.duplex.write(Message.escape(buffer)); + return true; + } + + close() { + if (this.isDead) return false; + this.duplex.end(); + return true; + } + + remoteClose() { + if (this.isDead) return false; + this.duplex.write(CLOSE_TOKEN); + return true; + } +} diff --git a/packages/duplex/src/common/errorserializer.ts b/packages/duplex/src/common/errorserializer.ts new file mode 100644 index 0000000..8d9de71 --- /dev/null +++ b/packages/duplex/src/common/errorserializer.ts @@ -0,0 +1,37 @@ +export interface SerializedError { + name: string; + message: string; + stack: string; + [prop: string]: any; +} + +export class ErrorSerializer { + // Converts an Error into a standard object. + static serialize(error: Error): SerializedError { + const { message, name, stack } = error; + return { message, name, stack, ...error }; + } + + // Converts an object into an Error instance. + static deserialize (data: SerializedError) { + const Factory = this.getFactory(data); + + const error = new Factory(data.message); + Object.assign(error, data); + + return error; + } + + // Tries to find the global class for the error name and + // returns Error if none is found. + private static getFactory (data: SerializedError): new (message: string) => Error { + const name = data.name; + + if (name.endsWith("Error") && global[name]) { + return global[name]; + } + + return Error; + } +} + diff --git a/packages/duplex/src/common/message.ts b/packages/duplex/src/common/message.ts new file mode 100644 index 0000000..5afafcf --- /dev/null +++ b/packages/duplex/src/common/message.ts @@ -0,0 +1,65 @@ +export const NEWLINE = Buffer.from("\n")[0]; +const ESC = Buffer.from("\\")[0]; +const ESC_N = Buffer.from("n")[0]; + +export class Message { + // Escape all newlines and backslashes in a Buffer. + static escape(data: Buffer): Buffer { + const result: number[] = []; + + for (const char of data) { + switch (char) { + case ESC: + // Escape the escaped backslash + result.push(ESC); + result.push(ESC); + break; + case NEWLINE: + // Escape newline + result.push(ESC); + result.push(ESC_N); + break; + default: + result.push(char); + break; + } + } + + result.push(NEWLINE); + + return Buffer.from(result); + } + + // Undoes what the escape method does. + static unescape(data: Buffer): Buffer { + const result: number[] = []; + + // Ignore last byte because it's the separating newline. + for (let i = 0; i < data.length - 1; i++) { + const char = data[i]; + const next = data[i + 1]; + + if (char === ESC) { + switch (next) { + case ESC: + // Escaped escaped backslash. + result.push(ESC); + i += 1; + break; + case ESC_N: + // Escaped newline. + result.push(NEWLINE); + i += 1; + break; + default: + throw new Error("Unescaped backslash detected!"); + } + } else { + result.push(char); + } + } + + return Buffer.from(result); + } +} + diff --git a/packages/duplex/src/common/status.ts b/packages/duplex/src/common/status.ts new file mode 100644 index 0000000..20820f9 --- /dev/null +++ b/packages/duplex/src/common/status.ts @@ -0,0 +1,6 @@ +export enum Status { + ONLINE = 3, + CONNECTING = 2, + CLOSED = 1, + OFFLINE = 0, +} diff --git a/packages/duplex/src/example/client.ts b/packages/duplex/src/example/client.ts new file mode 100644 index 0000000..158dc44 --- /dev/null +++ b/packages/duplex/src/example/client.ts @@ -0,0 +1,27 @@ +import { CommandClient } from "../client/commandclient"; +import { CodeError } from "../common/codeerror"; + +const client = new CommandClient({ + host: "localhost", + port: 3351, + secure: false, +}); + +const payload = { things: "stuff", numbers: [1, 2, 3] }; + +async function main() { + const callback = (result: any, error: CodeError) => { + if (error) { + console.log("ERR [0]", error.code); + return; + } + + console.log("RECV [0]", result); + client.close(); + }; + + client.command(0, payload, 10, callback); + +} + +main(); diff --git a/packages/duplex/src/example/server.ts b/packages/duplex/src/example/server.ts new file mode 100644 index 0000000..d4b5a83 --- /dev/null +++ b/packages/duplex/src/example/server.ts @@ -0,0 +1,18 @@ +import { CodeError } from "../common/codeerror"; +import { Connection } from "../common/connection"; +import { CommandServer } from "../server/commandserver"; + +const server = new CommandServer({ + host: "localhost", + port: 3351, + secure: false, +}); + +server.command(0, async (payload: any, connection: Connection) => { + console.log("RECV [0]:", payload); + return { ok: "OK" }; +}); + +server.on("clientError", (error: CodeError) => { + console.log("clientError", error.code); +}); diff --git a/packages/duplex/src/index.ts b/packages/duplex/src/index.ts new file mode 100644 index 0000000..f6a35aa --- /dev/null +++ b/packages/duplex/src/index.ts @@ -0,0 +1,5 @@ +export { CommandClient, type TokenClientOptions } from "./client/commandclient"; +export { CommandServer, type TokenServerOptions } from "./server/commandserver"; +export { Connection } from "./common/connection"; +export { CodeError } from "./common/codeerror"; +export { Status } from "./common/status"; diff --git a/packages/duplex/src/server/commandserver.ts b/packages/duplex/src/server/commandserver.ts new file mode 100644 index 0000000..2c89fad --- /dev/null +++ b/packages/duplex/src/server/commandserver.ts @@ -0,0 +1,179 @@ +import { EventEmitter } from "node:events"; +import net, { Socket } from "node:net"; +import tls from "node:tls"; +import { CodeError } from "../common/codeerror"; +import { Command } from "../common/command"; +import { Connection } from "../common/connection"; +import { ErrorSerializer } from "../common/errorserializer"; +import { Status } from "../common/status"; + +export type TokenServerOptions = tls.TlsOptions & net.ListenOptions & net.SocketConstructorOpts & { + secure?: boolean; +}; + +export class TokenServer extends EventEmitter { + connections: Connection[] = []; + + public options: TokenServerOptions; + public server: tls.Server | net.Server; + private hadError: boolean; + + status: Status; + + constructor(options: TokenServerOptions) { + super(); + + this.options = options; + + if (this.options.secure) { + this.server = tls.createServer(this.options, function (clientSocket) { + clientSocket.on("error", (err) => { + this.emit("clientError", err); + }); + }) + } else { + this.server = net.createServer(this.options, function (clientSocket) { + clientSocket.on("error", (err) => { + this.emit("clientError", err); + }); + }); + } + + this.applyListeners(); + this.connect(); + } + + connect(callback?: () => void) { + if (this.status >= Status.CONNECTING) return false; + + this.hadError = false; + this.status = Status.CONNECTING; + this.server.listen(this.options, () => { + if (callback) callback(); + }); + return true; + } + + close(callback?: () => void) { + if (!this.server.listening) return false; + + this.status = Status.CLOSED; + this.server.close(() => { + for (const connection of this.connections) { + connection.remoteClose(); + } + if (callback) callback(); + }); + + return true; + } + + applyListeners() { + this.server.on("listening", () => { + this.status = Status.ONLINE; + this.emit("listening"); + }); + + this.server.on("tlsClientError", (error) => { + this.emit("clientError", error); + }); + + this.server.on("clientError", (error) => { + this.emit("clientError", error); + }); + + this.server.on("error", (error) => { + this.hadError = true; + this.emit("error", error); + this.server.close(); + }); + + this.server.on("close", () => { + this.status = Status.OFFLINE; + this.emit("close", this.hadError); + }); + + this.server.on("secureConnection", (socket: Socket) => { + const connection = new Connection(socket); + this.connections.push(connection); + + connection.once("close", () => { + const i = this.connections.indexOf(connection); + if (i !== -1) this.connections.splice(i, 1); + }); + + connection.on("token", (token) => { + this.emit("token", token, connection); + }); + }); + + this.server.on("connection", (socket: Socket) => { + if (this.options.secure) return; + + const connection = new Connection(socket); + this.connections.push(connection); + + connection.once("close", () => { + const i = this.connections.indexOf(connection); + if (i !== -1) this.connections.splice(i, 1); + }); + + connection.on("token", (token) => { + this.emit("token", token, connection); + }); + }); + } +} + +type CommandFn = (payload: any, connection: Connection) => Promise; + +export class CommandServer extends TokenServer { + private commands: { + [command: number]: CommandFn + } = {}; + + constructor(options: TokenServerOptions) { + super(options); + this.init(); + } + + private init() { + this.on("token", async (buffer, connection) => { + try { + const { id, command, payload } = Command.parse(buffer); + this.runCommand(id, command, payload, connection); + } catch (error) { + this.emit("error", error); + } + }); + } + + /** + * @param command - The command number to register, a UInt8 (0-255). + * 255 is reserved. You will get an error if you try to use it. + * @param fn - The function to run when the command is received. + */ + command(command: number, fn: CommandFn) { + this.commands[command] = fn; + } + + private async runCommand(id: number, command: number, payload: any, connection: Connection) { + try { + if (!this.commands[command]) { + throw new CodeError(`Command (${command}) not found.`, "ENOTFOUND", "CommandError"); + } + + const result = await this.commands[command](payload, connection); + + // A payload should not be undefined, so if a command returns nothing + // we respond with a simple "OK". + const payloadResult = result === undefined ? "OK" : result; + + connection.send(Command.toBuffer({ command, id, payload: payloadResult })); + } catch (error) { + const payload = ErrorSerializer.serialize(error); + + connection.send(Command.toBuffer({ command: 255, id, payload })); + } + } +} diff --git a/packages/duplex/src/server/ids.ts b/packages/duplex/src/server/ids.ts new file mode 100644 index 0000000..ca1cbae --- /dev/null +++ b/packages/duplex/src/server/ids.ts @@ -0,0 +1,40 @@ +export class IdManager { + ids: Array = []; + index: number = 0; + maxIndex: number; + + constructor(maxIndex: number = 2 ** 16 - 1) { + this.maxIndex = maxIndex; + } + + release(id: number) { + if (id < 0 || id > this.maxIndex) { + throw new TypeError(`ID must be between 0 and ${this.maxIndex}. Got ${id}.`); + } + this.ids[id] = false; + } + + reserve(): number { + const startIndex = this.index; + + while (true) { + const i = this.index; + + if (!this.ids[i]) { + this.ids[i] = true; + + return i; + } + + if (this.index >= this.maxIndex) { + this.index = 0; + } else { + this.index++; + } + + if (this.index === startIndex) { + throw new Error(`All IDs are reserved. Make sure to release IDs when they are no longer used.`); + } + } + } +} diff --git a/packages/duplex/tsconfig.json b/packages/duplex/tsconfig.json new file mode 100644 index 0000000..7fc5833 --- /dev/null +++ b/packages/duplex/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "esnext", + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist" + } +} diff --git a/packages/duplex/tsup.config.ts b/packages/duplex/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/duplex/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +}); diff --git a/packages/express-keepalive-ws/.npmignore b/packages/express-keepalive-ws/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/express-keepalive-ws/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/express-keepalive-ws/README.md b/packages/express-keepalive-ws/README.md new file mode 100644 index 0000000..fff6e6d --- /dev/null +++ b/packages/express-keepalive-ws/README.md @@ -0,0 +1,43 @@ +# @prsm/express-keepalive-ws + +This is a middleware that creates and exposes a `KeepAliveServer` instance (see [prsm/keepalive-ws](https://github.com/...). + +```typescript +import express from "express"; +import createWss, { type WSContext } from "@prsm/express-keepalive-ws"; + +const app = express(); +const server = createServer(app); + +const { middleware: ws, wss } = createWss({ /** ... */ }); + +app.use(ws); + +// as a middleware: +app.use("/ws", async (req, res) => { + if (req.ws) { // <-- req.ws will be defined if the request is a WebSocket request + const ws = await req.ws(); // handle the upgrade and receive the client WebSocket + ws.send("Hello WS!"); // send a message to the client + } else { + res.send("Hello HTTP!"); + } +}); + +// as a command server: +wss.registerCommand("echo", (c: WSContext) => { + const { payload } = c; + return `echo: ${payload}`; +}); +``` + +Client-side usage (more at https://github.com/node-prism/keepalive-ws): + +```typescript +import { KeepAliveClient } from "@prsm/keepalive-ws/client"; + +const opts = { shouldReconnect: true }; +const ws = new KeepAliveClient("ws://localhost:PORT", opts); + +const echo = await ws.command("echo", "hello!"); +console.log(echo); // "echo: hello!" +``` diff --git a/packages/express-keepalive-ws/bump.config.ts b/packages/express-keepalive-ws/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/express-keepalive-ws/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/express-keepalive-ws/bun.lockb b/packages/express-keepalive-ws/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..8a74d166cac36128529f46ca31cb3d1f50bc0ec1 GIT binary patch literal 78303 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 literal 0 HcmV?d00001 diff --git a/packages/express-keepalive-ws/package.json b/packages/express-keepalive-ws/package.json new file mode 100644 index 0000000..0c48c99 --- /dev/null +++ b/packages/express-keepalive-ws/package.json @@ -0,0 +1,30 @@ +{ + "name": "@prsm/express-keepalive-ws", + "version": "1.2.0", + "author": "", + "main": "./dist/index.js", + "devDependencies": { + "@types/ws": "^8.5.12", + "bumpp": "^9.5.1", + "tsup": "^8.2.4", + "typescript": "^5.5.4" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "description": "", + "keywords": [], + "license": "Apache-2.0", + "scripts": { + "build": "tsup", + "release": "bumpp package.json && npm publish --access public" + }, + "type": "module", + "dependencies": { + "@prsm/keepalive-ws": "^0.3.1" + } +} diff --git a/packages/express-keepalive-ws/src/index.ts b/packages/express-keepalive-ws/src/index.ts new file mode 100644 index 0000000..2607d94 --- /dev/null +++ b/packages/express-keepalive-ws/src/index.ts @@ -0,0 +1,65 @@ +import { + KeepAliveServer, + type KeepAliveServerOptions, +} from "@prsm/keepalive-ws/server"; +import { type Server } from "node:http"; +import { STATUS_CODES } from "node:http"; + +const createWsMiddleware = ( + server: Server, + options: KeepAliveServerOptions = {}, +): { middleware: (req, res, next) => Promise; wss: KeepAliveServer } => { + const wss = new KeepAliveServer({ ...options, noServer: true }); + + server.on("upgrade", (request, socket, head) => { + const { pathname } = new URL(request.url, `http://${request.headers.host}`); + + const path = options.path || "/"; + + if (pathname !== path) { + socket.write( + [ + `HTTP/1.0 400 ${STATUS_CODES[400]}`, + "Connection: close", + "Content-Type: text/html", + `Content-Length: ${Buffer.byteLength(STATUS_CODES[400])}`, + "", + STATUS_CODES[400], + ].join("\r\n"), + ); + + socket.destroy(); + + return; + } + + wss.handleUpgrade(request, socket, head, (client, req) => { + wss.emit("connection", client, req); + }); + }); + + const middleware = async (req, res, next) => { + const upgradeHeader: string[] = + req.headers.upgrade + ?.toLowerCase() + .split(",") + .map((s) => s.trim()) || []; + + if (upgradeHeader.includes("websocket")) { + req.ws = () => + new Promise((resolve) => { + wss.handleUpgrade(req, req.socket, Buffer.alloc(0), (client) => { + wss.emit("connection", client, req); + resolve(client); + }); + }); + } + + await next(); + }; + + return { middleware, wss }; +}; + +export default createWsMiddleware; +export { type WSContext } from "@prsm/keepalive-ws/server"; diff --git a/packages/express-keepalive-ws/tsconfig.json b/packages/express-keepalive-ws/tsconfig.json new file mode 100644 index 0000000..7fc5833 --- /dev/null +++ b/packages/express-keepalive-ws/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "esnext", + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist" + } +} diff --git a/packages/express-keepalive-ws/tsup.config.ts b/packages/express-keepalive-ws/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/express-keepalive-ws/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +}); diff --git a/packages/express-session-auth/.npmignore b/packages/express-session-auth/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/express-session-auth/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/express-session-auth/README.md b/packages/express-session-auth/README.md new file mode 100644 index 0000000..cfacac3 --- /dev/null +++ b/packages/express-session-auth/README.md @@ -0,0 +1,86 @@ +# express-session-auth + +## Requirements + +- `express-session`: https://github.com/expressjs/session +- `cookie-parser`: https://github.com/expressjs/cookie-parser +- TypeORM + - `express-session-auth` exports entities (`User`, `UserReset`, `UserRemember`, `UserConfirmation`) that you need to include in your datasource for migration/sync purposes. + + +## Quickstart + +Wherever you create your express application, include the auth middleware and pass in your TypeORM datasource. + +```typescript +import express from "express"; +import { createServer } from "node:http"; +import auth from "@prsm/express-session-auth"; +import datasource from "./my-datasource"; + +const app = express(); +const server = createServer(app); + +// the auth middleware needs your datasource instance +app.use(auth({ datasource })); +``` + +Here's an example TypeORM datasource: + +```typescript +// my-datasource.ts +import { + User, + UserConfirmation, + UserRemember, + UserReset, +} from "@prsm/express-session-auth"; +import { DataSource } from "typeorm"; + +const datasource = new DataSource({ + type: "mysql", // express-session-auth supports mysql, postgres and sqlite (others not tested) + host: process.env.DB_HOST, + port: process.env.DB_PORT ? +process.env.DB_PORT : 3306, + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: [ + User, + UserConfirmation, + UserRemember, + UserReset, + /* the reset of your entities here */ + ], +}); + +export default datasource; +``` + +Environment variables and their defaults: + +```bash +HTTP_PORT=3002 + +AUTH_SESSION_REMEMBER_DURATION=30d +AUTH_SESSION_REMEMBER_COOKIE_NAME=prsm.auth.remember +AUTH_SESSION_RESYNC_INTERVAL=30m +AUTH_MINIMUM_PASSWORD_LENGTH=8 +AUTH_MAXIMUM_PASSWORD_LENGTH=64 + +DB_HOST=localhost +DB_PORT=3306 +DB_USERNAME=root +DB_PASSWORD=toor +DB_NAME=prsm +``` + +Because this middleware augments the `Request` object by adding an `auth` property, you will want to add the following to your `tsconfig.json` so that your language server doesn't flag references to `req.auth` as an error: + +```json +{ + "include": [ + "src", + "node_modules/@prsm/express-session-auth/express-session-auth.d.ts" + ] +} +``` diff --git a/packages/express-session-auth/bump.config.ts b/packages/express-session-auth/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/express-session-auth/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/express-session-auth/bun.lockb b/packages/express-session-auth/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..1b5b011f968c0a57adce98aee89fbba6e61c1467 GIT binary patch literal 122206 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 + authAdmin: ReturnType; + } + } +} diff --git a/packages/express-session-auth/package.json b/packages/express-session-auth/package.json new file mode 100644 index 0000000..a922ac7 --- /dev/null +++ b/packages/express-session-auth/package.json @@ -0,0 +1,46 @@ +{ + "name": "@prsm/express-session-auth", + "version": "1.5.0", + "description": "", + "main": "./dist/index.js", + "type": "module", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "express-session-auth.d.ts" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "release": "bumpp package.json && npm publish --access public" + }, + "keywords": [], + "author": "", + "peerDependencies": { + "typeorm": "0.3.20" + }, + "license": "Apache-2.0", + "dependencies": { + "@prsm/hash": "^1.0.2", + "@prsm/ids": "^1.1.1", + "@prsm/ms": "^1.0.1", + "@types/express": "^4.17.21", + "cookie-parser": "^1.4.6", + "express": "^4.19.2", + "express-session": "^1.18.0" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.7", + "@types/express-session": "^1.18.0", + "@types/node": "^22.4.1", + "bumpp": "^9.5.1", + "tsup": "^8.2.4", + "typescript": "^5.5.4" + } +} diff --git a/packages/express-session-auth/src/errors.ts b/packages/express-session-auth/src/errors.ts new file mode 100644 index 0000000..882c076 --- /dev/null +++ b/packages/express-session-auth/src/errors.ts @@ -0,0 +1,118 @@ +export class ConfirmationNotFoundError extends Error { + constructor(message: string = "Confirmation selector/token pair not found") { + super(message); + this.name = "ConfirmationNotFoundError"; + } +} + +export class ConfirmationExpiredError extends Error { + constructor(message: string = "Confirmation selector/token pair expired") { + super(message); + this.name = "ConfirmationExpiredError"; + } +} + +export class EmailTakenError extends Error { + constructor(message: string = "Email already exists") { + super(message); + this.name = "EmailTakenError"; + } +} + +export class EmailNotVerifiedError extends Error { + constructor(message: string = "User not verified") { + super(message); + this.name = "EmailNotVerifiedError"; + } +} + +export class ImpersonationNotAllowedError extends Error { + constructor(message: string = "Impersonation not allowed") { + super(message); + this.name = "ImpersonationNotAllowedError"; + } +} + +export class InvalidEmailError extends Error { + constructor(message: string = "Invalid email provided") { + super(message); + this.name = "InvalidEmailError"; + } +} + +export class InvalidPasswordError extends Error { + constructor(message: string = "Invalid password provided") { + super(message); + this.name = "InvalidPasswordError"; + } +} + +export class InvalidTokenError extends Error { + constructor(message: string = "Invalid selector/token pair provided") { + super(message); + this.name = "InvalidSelectorTokenPairError"; + } +} + +export class InvalidUsernameError extends Error { + constructor(message: string = "Invalid username provided") { + super(message); + this.name = "InvalidUsernameError"; + } +} + +export class ResetDisabledError extends Error { + constructor(message: string = "Password reset is disabled") { + super(message); + this.name = "ResetDisabledError"; + } +} + +export class ResetExpiredError extends Error { + constructor(message: string = "Reset request expired") { + super(message); + this.name = "ResetExpiredError"; + } +} + +export class ResetNotFoundError extends Error { + constructor(message: string = "Reset request not found") { + super(message); + this.name = "ResetNotFoundError"; + } +} + +export class TooManyResetsError extends Error { + constructor(message: string = "Too many resets") { + super(message); + this.name = "TooManyResetsError"; + } +} + +export class UserInactiveError extends Error { + constructor(message: string = "User is inactive") { + super(message); + this.name = "UserInactiveError"; + } +} + +export class UserNotFoundError extends Error { + constructor(message: string = "User not found") { + super(message); + this.name = "UserNotFoundError"; + } +} + +export class UserNotLoggedInError extends Error { + constructor(message: string = "User not logged in") { + super(message); + this.name = "UserNotLoggedInError"; + } +} + +export class UsernameTakenError extends Error { + constructor(message: string = "Username already exists") { + super(message); + this.name = "UsernameTakenError"; + } +} diff --git a/packages/express-session-auth/src/index.ts b/packages/express-session-auth/src/index.ts new file mode 100644 index 0000000..d33bd8b --- /dev/null +++ b/packages/express-session-auth/src/index.ts @@ -0,0 +1,5 @@ +export { middleware as default } from "./middleware.js"; +export { User, AuthStatus, AuthRole } from "./user.entity.js"; +export { UserReset } from "./user-reset.entity.js"; +export { UserConfirmation } from "./user-confirmation.entity.js"; +export { UserRemember } from "./user-remember.entity.js"; diff --git a/packages/express-session-auth/src/middleware.ts b/packages/express-session-auth/src/middleware.ts new file mode 100644 index 0000000..81eb571 --- /dev/null +++ b/packages/express-session-auth/src/middleware.ts @@ -0,0 +1,1489 @@ +import { hash } from "@prsm/hash"; +import ID from "@prsm/ids"; +import ms from "@prsm/ms"; +import type { Request, Response } from "express"; +import { LessThanOrEqual, MoreThanOrEqual, type DataSource } from "typeorm"; +import { + ConfirmationExpiredError, + ConfirmationNotFoundError, + EmailNotVerifiedError, + EmailTakenError, + InvalidEmailError, + InvalidPasswordError, + InvalidTokenError, + InvalidUsernameError, + ResetDisabledError, + ResetExpiredError, + ResetNotFoundError, + TooManyResetsError, + UserInactiveError, + UsernameTakenError, + UserNotFoundError, + UserNotLoggedInError, +} from "./errors.js"; +import { UserConfirmation } from "./user-confirmation.entity.js"; +import { UserRemember } from "./user-remember.entity.js"; +import { UserReset } from "./user-reset.entity.js"; +import { AuthStatus, getRoleMap, getStatusMap, User } from "./user.entity.js"; +import { ensureRequiredMiddlewares, isValidEmail } from "./util.js"; + +declare module "express-session" { + export interface SessionData { + auth: AuthSession; + } +} + +interface AuthenticatedRequest extends Request { + auth: Awaited>; + authAdmin: ReturnType; +} + +type AuthSession = { + loggedIn: boolean; + userId: string; + email: string; + username: string; + status: number; + rolemask: number; + remembered: boolean; + lastResync: Date; + forceLogout: number; + verified: boolean; +}; + +type ReqResDatasource = { + req: Request; + res: Response; + datasource: DataSource; +}; + +type TokenCallback = (token: string) => void; + +type CreateUserOptions = { + requireUsername: boolean; + email: string; + password: string; + username?: string; + callback?: TokenCallback; +}; + +const validateEmail = (email: string) => { + if (typeof email !== "string") { + throw new InvalidEmailError(); + } + if (!email.trim()) { + throw new InvalidEmailError(); + } + if (!isValidEmail(email)) { + throw new InvalidEmailError(); + } +}; + +const validatePassword = (password: string) => { + const minLength = process.env.AUTH_MINIMUM_PASSWORD_LENGTH + ? +process.env.AUTH_MINIMUM_PASSWORD_LENGTH + : 8; + + const maxLength = process.env.AUTH_MAXIMUM_PASSWORD_LENGTH + ? +process.env.AUTH_MAXIMUM_PASSWORD_LENGTH + : 64; + + if (typeof password !== "string") { + throw new InvalidPasswordError(); + } + + if (password.length < minLength) { + throw new InvalidPasswordError(); + } + + if (password.length > maxLength) { + throw new InvalidPasswordError(); + } +}; + +const createUserManager = ({ req, res, datasource }: ReqResDatasource) => { + const userRepository = () => datasource.getRepository(User); + const userConfirmationRepository = () => + datasource.getRepository(UserConfirmation); + const userRememberRepository = () => datasource.getRepository(UserRemember); + const userResetRepository = () => datasource.getRepository(UserReset); + + const getByUsername = (username: string) => + userRepository().findOne({ where: { username } }); + const getByEmail = (email: string) => + userRepository().findOne({ where: { email } }); + const getById = (id: number) => userRepository().findOne({ where: { id } }); + + /** + * Operates on session.auth. + */ + const hasRole = async (role: number) => { + if (req.session.auth) { + return (req.session.auth.rolemask & role) === role; + } + + const user = await getUser(); + return (user.rolemask & role) === role; + }; + + /** + * Operates on session.auth. + */ + const isRemembered = () => req.session.auth?.remembered ?? false; + + /** + * Operates on session.auth. + */ + const isAdmin = async () => hasRole(1); + + const getSessionProperty = (property: PropertyKey) => { + return req.session?.auth ? req.session.auth[property] : null; + }; + + /** Returns the logged-in user's `id` property. */ + const getId = () => + getSessionProperty("userId") + ? ID.decode(getSessionProperty("userId")) + : null; + + /** + * Operates on session.auth. + * Returns the logged-in user's `email` property. + */ + const getEmail = () => getSessionProperty("email"); + + /** + * Operates on session.auth. + * Returns the logged-in user's `status` property. + */ + const getStatus = (): number => getSessionProperty("status"); + + /** + * Operates on session.auth. + * Returns the logged-in user's `verified` property. + */ + const getVerified = (): number => getSessionProperty("verified"); + + /** + * Operates on session.auth. + * Returns the logged-in user. + */ + const getUser = async () => { + const userId = getId(); + + if (!userId) { + return null; + } + + const user = await userRepository().findOne({ where: { id: userId } }); + + if (!user) { + return null; + } + + return user; + }; + + /** + * Operates on session.auth. + */ + const getRoleNames = (rolemask?: number) => { + const mask = + rolemask === undefined ? getSessionProperty("rolemask") : rolemask; + + if (!mask && mask !== 0) { + return []; + } + + return Object.entries(getRoleMap()) + .filter(([key, value]) => mask & parseInt(key)) + .map(([key, value]) => value); + }; + + /** + * Operates on session.auth. + */ + const getStatusName = () => { + const status = getStatus(); + return getStatusMap()[status]; + }; + + const createUserInternal = async ({ + requireUsername, + email, + password, + username, + callback, + }: CreateUserOptions) => { + validateEmail(email); + validatePassword(password); + + const trimmedUsername = username?.trim(); + + if (trimmedUsername === "") { + throw new InvalidUsernameError(); + } + + if (requireUsername && trimmedUsername) { + const existingUser = await getByUsername(username); + + if (existingUser) { + throw new UsernameTakenError(); + } + } + + const existingUser = await userRepository().findOne({ where: { email } }); + + if (existingUser) { + throw new EmailTakenError(); + } + + const hashedPassword = hash.encode(password); + const verified = typeof callback !== "function"; + + const user = userRepository().create({ + email, + password: hashedPassword, + username: trimmedUsername, + verified, + status: AuthStatus.Normal, + resettable: true, + rolemask: 0, + registered: new Date(), + lastLogin: null, + forceLogout: 0, + }); + + await userRepository().save(user); + + if (!verified) { + await createConfirmationToken(user, email, callback); + } + + return user; + }; + + const createConfirmationToken = async ( + user: User, + email: string, + callback: TokenCallback, + ) => { + const token = hash.encode(email); + const expires = new Date( + Date.now() + 1000 * 60 * 60 * 24 * 7, // 1 week + ); + + await userConfirmationRepository().delete({ user }); + + const confirmation = userConfirmationRepository().create({ + user, + token, + expires, + email, + }); + + await userConfirmationRepository().save(confirmation); + + if (callback) { + callback(token); + } + }; + + const recreateConfirmationTokenForUserId = async ( + userId: number, + callback: TokenCallback, + ) => { + const user = await getById(userId); + + if (!user) { + throw new UserNotFoundError(); + } + + return recreateConfirmationToken(user, callback); + }; + + const recreateConfirmationTokenForEmail = async ( + email: string, + callback: TokenCallback, + ) => { + const user = await getByEmail(email); + + if (!user) { + throw new UserNotFoundError(); + } + + return recreateConfirmationToken(user, callback); + }; + + const recreateConfirmationToken = async ( + user: User, + callback: TokenCallback, + ) => { + const latestAttempt = await userConfirmationRepository().findOne({ + where: { user }, + order: { expires: "DESC" }, + }); + + if (!latestAttempt) { + throw new ConfirmationNotFoundError(); + } + + await createConfirmationToken(user, latestAttempt.email, callback); + }; + + const createRememberDirective = async (user: User) => { + const token = hash.encode(user.email); + const expires = new Date( + Date.now() + ms(process.env.AUTH_SESSION_REMEMBER_DURATION), + ); + + await userRememberRepository().delete({ user }); + + await userRememberRepository().insert({ + user, + token, + expires, + }); + + setRememberCookie(token, expires); + + return token; + }; + + const setRememberCookie = (token: string, expires: Date) => { + const cookieName = process.env.AUTH_SESSION_REMEMBER_COOKIE_NAME; + const cookieOptions = { expires, httpOnly: true, secure: false }; + res.cookie(cookieName, token, cookieOptions); + }; + + /** + * Registers a new user with the provided email, password, and optional username. + * + * - When a callback is provided, the user's `verified` property will be set to `0` and a confirmation token will be created. + * The token will be passed to the callback. You should email the token to the user and have a route that accepts + * the token and then calls `confirmEmail` or `confirmEmailAndLogin` with it. + * + * @throws {InvalidEmailError} When the provided email is invalid. + * @throws {InvalidPasswordError} When the provided password is invalid. + * @throws {EmailTakenError} When the provided email is already in use. + * @throws {InvalidUsernameError} When the provided username is invalid. + */ + const register = async ( + email: string, + password: string, + username?: string, + callback?: TokenCallback, + ) => + createUserInternal({ + requireUsername: false, + email, + password, + username, + callback, + }); + + const registerWithUniqueUsername = async ( + email: string, + password: string, + username: string, + callback: TokenCallback, + ) => + createUserInternal({ + requireUsername: true, + email, + password, + username, + callback, + }); + + return { + register, + registerWithUniqueUsername, + + getId, + getEmail, + getStatus, + getVerified, + getRoleNames, + getStatusName, + + getUser, + + getById, + getByEmail, + getByUsername, + + userRepository, + userResetRepository, + userConfirmationRepository, + userRememberRepository, + + setRememberCookie, + createRememberDirective, + + createConfirmationToken, + recreateConfirmationTokenForEmail, + recreateConfirmationTokenForUserId, + + isAdmin, + hasRole, + isRemembered, + }; +}; + +export const createAuth = async ({ + req, + res, + datasource, +}: ReqResDatasource) => { + if (!datasource) { + throw new Error("datasource is required"); + } + const um = createUserManager({ req, res, datasource }); + + const isLoggedIn = () => req.session?.auth?.loggedIn ?? false; + + /** + * Resynchronizes the session with the latest user data. + * + * - Does nothing if the user is not logged in. + * - Resynchronizes only if the last resync was before the configured interval. + * - Logs out the user if the user cannot be found. + * - Logs out the user if the forceLogout value in the database is greater than the session's forceLogout value. + * + * @throws {Error} When session regeneration fails. + */ + const resyncSession = async () => { + if (!isLoggedIn()) { + return; + } + + const interval = ms(process.env.AUTH_SESSION_RESYNC_INTERVAL || "30m"); + + const lastResync = new Date(req.session.auth.lastResync); + + if (lastResync && lastResync.getTime() > Date.now() - interval) { + return; + } + + const user = await um.getUser(); + + if (!user) { + await logout(); + return; + } + + if (user.forceLogout > req.session.auth.forceLogout) { + await logout(); + return; + } + + req.session.auth.email = user.email; + req.session.auth.username = user.username; + req.session.auth.status = user.status; + req.session.auth.rolemask = user.rolemask; + req.session.auth.verified = user.verified; + req.session.auth.lastResync = new Date(); + }; + + await resyncSession(); + + const processRememberDirective = async () => { + if (!isLoggedIn()) { + return; + } + + const { token } = getRememberToken(); + + if (!token) { + return; + } + + const directive = await um + .userRememberRepository() + .findOne({ where: { token } }); + + if (!directive) { + return; + } + + if (!directive.user) { + await logout(); + return; + } + + // remove expired directives for this user + const expiredRemembers = await um.userRememberRepository().find({ + where: { user: directive.user, expires: LessThanOrEqual(new Date()) }, + }); + await um.userRememberRepository().remove(expiredRemembers); + + // is this directive expired? + if (new Date() > directive.expires) { + await um.userRememberRepository().remove(directive); + um.setRememberCookie(null, new Date(0)); + return; + } + + // okay to login + await onLoginSuccessful(directive.user, true); + }; + + /** + * Logs in a user with the provided email and password. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided email. + * @throws {InvalidPasswordError} When the provided password is incorrect. + * @throws {EmailNotVerifiedError} When the user's email is not verified. + * @throws {UserInactiveError} When the user's status is not normal. + */ + const login = async (email: string, password: string, remember = false) => + loginWithCredentials({ email, password, remember }); + + const loginWithCredentials = async (credentials: { + email: string; + password: string; + username?: string; + remember: boolean; + }) => { + const user = credentials.email + ? await um.getByEmail(credentials.email) + : await um.getByUsername(credentials.username); + + if (!user) { + throw new UserNotFoundError(); + } + + if (!hash.verify(user.password, credentials.password)) { + throw new InvalidPasswordError(); + } + + if (!user.verified) { + throw new EmailNotVerifiedError(); + } + + if (user.status !== AuthStatus.Normal) { + throw new UserInactiveError(); + } + + await onLoginSuccessful(user, credentials.remember); + }; + + /** + * Logs out the currently logged-in user. + * + * - Deletes the remember token if it exists. + * - Clears the remember cookie. + * + * @throws {Error} When session regeneration fails. + */ + const logout = async () => { + if (!isLoggedIn()) { + return; + } + + const { token } = getRememberToken(); + + if (token) { + await um.userRememberRepository().delete({ token }); + um.setRememberCookie(null, new Date(0)); + } + + req.session.auth = undefined; + }; + + /** + * Forces logout for a user identified by id. + * + * - Increments the forceLogout counter for the user. + * + * @throws {TypeError} When the provided id is not a number. + */ + const forceLogoutForUserById = async (id: number) => { + if (typeof id !== "number") { + throw new TypeError("User ID must be a number"); + } + + await um.userRememberRepository().delete({ user: { id } }); + await um.userRepository().increment({ id }, "forceLogout", 1); + }; + + /** + * Forces logout for the currently logged-in user. + */ + const forceLogoutForUser = async () => { + const userId = um.getId(); + + if (userId) { + await forceLogoutForUserById(userId); + } + }; + + /** + * Logs out the user from all sessions except the current one. + * + * - Increments the forceLogout counter for the user. + * - Regenerates the session to apply the forceLogout change. + * + * Since this session's forceLogout value will not be greater than the + * value in the database, the user will be logged out from all other sessions, + * but not from the current one. See resyncSession for clarity on this behavior. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const logoutEverywhereElse = async () => { + if (!isLoggedIn()) { + return; + } + + const userId = um.getId(); + + const user = await um.getById(userId); + + if (!user) { + await logout(); + return; + } + + await forceLogoutForUserById(userId); + + req.session.auth.forceLogout += 1; + + await regenerate(); + }; + + /** + * Logs out the user from all sessions, including the current one. + * + * - Calls `logoutEverywhereElse` to log out from all other sessions. + * - Calls `logout` to log out from the current session. + * + * @throws {Error} When session regeneration fails. + */ + const logoutEverywhere = async () => { + if (!isLoggedIn()) { + return; + } + + await logoutEverywhereElse(); + await logout(); + }; + + /** + * Regenerates the session while preserving the current auth data. + * + * - Copies the current session's auth data before regenerating the session. + * - Restores the auth data to the new session. + * + * @throws {Error} When session regeneration fails. + */ + const regenerate = async () => { + const auth = { ...req.session.auth }; + + return new Promise((resolve, reject) => { + req.session.regenerate((err) => { + if (err) { + reject(err); + return; + } + req.session.auth = auth; + resolve(); + }); + }); + }; + + const getRememberToken = () => { + if (!req.cookies) { + return { token: null }; + } + + const cookieName = process.env.AUTH_SESSION_REMEMBER_COOKIE_NAME; + const token = req.cookies[cookieName]; + + if (!token) { + return { token: null }; + } + + return { token }; + }; + + const getRememberExpiry = async () => { + if (!isLoggedIn()) { + return; + } + + const { token } = getRememberToken(); + + if (!token) { + return null; + } + + const directive = await um + .userRememberRepository() + .findOne({ where: { token } }); + + return directive?.expires ?? null; + }; + + /** + * Handles successful login for a user. + * + * - Updates the user's last login timestamp. + * - Regenerates the session to prevent session fixation attacks. + * - Sets the session's auth data with user details. + * - Creates a remember directive if the remember option is true. + * + * @throws {Error} When session regeneration fails. + */ + const onLoginSuccessful = async (user: User, remember = false) => { + await um.userRepository().update(user.id, { lastLogin: new Date() }); + + return new Promise((resolve, reject) => { + if (!req.session?.regenerate) { + console.log( + "COULD NOT REGENERATE SESSION WTF. req:session:", + req.session, + ); + resolve(); + } + req.session.regenerate(async (err) => { + if (err) { + reject(err); + return; + } + + const session: AuthSession = { + loggedIn: true, + userId: ID.encode(user.id), + email: user.email, + username: user.username, + status: user.status, + rolemask: user.rolemask, + remembered: remember, + lastResync: new Date(), + forceLogout: user.forceLogout, + verified: user.verified, + }; + + req.session.auth = session; + + if (remember) { + await um.createRememberDirective(user); + } + + req.session.save((err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + }); + }; + + /** + * Initiates an email change for the logged-in user. + * + * - Sends a confirmation token to the new email address. + * + * @throws {UserNotLoggedInError} When no user is currently logged in. + * @throws {InvalidEmailError} When the provided email is invalid. + * @throws {EmailTakenError} When the provided email is already in use. + * @throws {UserNotFoundError} When the logged-in user cannot be found. + * @throws {EmailNotVerifiedError} When the logged-in user's email is not verified. + */ + const changeEmail = async (newEmail: string, callback: TokenCallback) => { + if (!isLoggedIn()) { + throw new UserNotLoggedInError(); + } + + validateEmail(newEmail); + + const existing = await um.getByEmail(newEmail); + + if (existing) { + throw new EmailTakenError(); + } + + const user = await um.getById(um.getId()); + + if (!user) { + throw new UserNotFoundError(); + } + + if (!user.verified) { + throw new EmailNotVerifiedError(); + } + + await um.createConfirmationToken(user, newEmail, callback); + }; + + /** + * Confirms an email change using the provided token. + * + * @throws {ConfirmationNotFoundError} When the confirmation token cannot be found. + * @throws {ConfirmationExpiredError} When the confirmation token has expired. + * @throws {InvalidTokenError} When the provided token is invalid. + */ + const confirmChangeEmail = async (token: string) => { + const confirmation = await um.userConfirmationRepository().findOne({ + where: { token }, + }); + + if (!confirmation) { + throw new ConfirmationNotFoundError(); + } + + if (new Date(confirmation.expires) < new Date()) { + throw new ConfirmationExpiredError(); + } + + if (!hash.verify(token, confirmation.email)) { + throw new InvalidTokenError(); + } + + await um.userRepository().update(confirmation.user.id, { + verified: true, + email: confirmation.email, + }); + + if ( + isLoggedIn() && + req.session?.auth?.userId === ID.encode(confirmation.user.id) + ) { + req.session.auth.verified = true; + req.session.auth.email = confirmation.email; + } + + await um.userConfirmationRepository().remove(confirmation); + + return confirmation.email; + }; + + /** + * Confirms an email change using the provided token. + * + * @throws {ConfirmationNotFoundError} When the confirmation token cannot be found. + * @throws {ConfirmationExpiredError} When the confirmation token has expired. + * @throws {InvalidTokenError} When the provided token is invalid. + */ + const confirmEmail = async (token: string) => confirmChangeEmail(token); + + /** + * Confirms an email change using the provided token and logs in the user. + * + * - Logs in the user if not already logged in. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided email. + */ + const confirmEmailAndLogin = async (token: string) => + confirmChangeEmailAndLogin(token); + + /** + * Confirms an email change using the provided token and logs in the user. + * + * - Logs in the user if not already logged in. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided email. + */ + const confirmChangeEmailAndLogin = async ( + token: string, + remember = false, + ) => { + const email = await confirmChangeEmail(token); + + if (isLoggedIn()) { + return; + } + + const user = await um.getByEmail(email); + + if (!user) { + throw new UserNotFoundError(); + } + + await onLoginSuccessful(user, remember); + }; + + /** + * Updates the password for the specified user. + * + * @throws {InvalidPasswordError} When the provided password is invalid. + */ + const updatePasswordInternal = async (user: User, password: string) => { + await um + .userRepository() + .update(user.id, { password: hash.encode(password) }); + }; + + /** + * Initiates a password reset for the user identified by email. + * + * - Limits the number of open reset requests to `maxOpenRequests`. + * + * @throws {EmailNotVerifiedError} When the user's email is not verified. + * @throws {ResetDisabledError} When the user's reset functionality is disabled. + * @throws {TooManyResetsError} When the user has too many open reset requests. + */ + const resetPassword = async ( + email: string, + expiresAfter: string | number | null, + maxOpenRequests: number | null, + callback?: TokenCallback, + ) => { + validateEmail(email); + expiresAfter = !expiresAfter ? ms("6h") : ms(expiresAfter); + maxOpenRequests = + maxOpenRequests === null ? 2 : Math.max(1, maxOpenRequests); + + const user = await um.userRepository().findOne({ where: { email } }); + + if (!user || !user.verified) { + throw new EmailNotVerifiedError(); + } + + if (!user.resettable) { + throw new ResetDisabledError(); + } + + // find all open, non-expired reset requests + const openRequests = await um + .userResetRepository() + .find({ where: { user, expires: MoreThanOrEqual(new Date()) } }); + + if (openRequests.length >= maxOpenRequests) { + throw new TooManyResetsError(); + } + + const token = hash.encode(email); + const expires = new Date(Date.now() + ms(expiresAfter)); + await um.userResetRepository().insert({ user, token, expires }); + }; + + /** + * Confirms a password reset using the provided token and sets a new password. + * + * - Logs out the user from all sessions if `logout` is true. + * + * @throws {ResetNotFoundError} When the reset token cannot be found. + * @throws {ResetExpiredError} When the reset token has expired. + * @throws {ResetDisabledError} When the user's reset functionality is disabled. + * @throws {InvalidTokenError} When the provided token is invalid. + * @throws {InvalidPasswordError} When the provided password is invalid. + */ + const confirmResetPassword = async ( + token: string, + password: string, + logout = true, + ) => { + const reset = await um + .userResetRepository() + .findOne({ where: { token }, order: { expires: "DESC" } }); + + if (!reset) { + throw new ResetNotFoundError(); + } + + if (new Date(reset.expires) < new Date()) { + throw new ResetExpiredError(); + } + + if (!reset.user.resettable) { + throw new ResetDisabledError(); + } + + validatePassword(password); + + if (!hash.verify(token, reset.user.email)) { + throw new InvalidTokenError(); + } + + await updatePasswordInternal(reset.user, password); + + if (logout) { + await forceLogoutForUserById(reset.user.id); + } + + await um.userResetRepository().remove(reset); + }; + + /** + * Verifies the provided password against the logged-in user's password. + * + * @throws {UserNotLoggedInError} When no user is currently logged in. + * @throws {UserNotFoundError} When the logged-in user cannot be found. + */ + const verifyPassword = async (password: string) => { + if (!isLoggedIn()) { + throw new UserNotLoggedInError(); + } + + const user = await um.getUser(); + + if (!user) { + throw new UserNotFoundError(); + } + + return hash.verify(user.password, password); + }; + + return { + processRememberDirective, + + forceLogoutForUser, + forceLogoutForUserById, + + login, + logout, + logoutEverywhere, + logoutEverywhereElse, + + register: um.register, + + changeEmail, + confirmEmail, + confirmEmailAndLogin, + confirmChangeEmail, + confirmChangeEmailAndLogin, + + resetPassword, + confirmResetPassword, + + verifyPassword, + + isAdmin: um.isAdmin, + hasRole: um.hasRole, + + isLoggedIn, + isRemembered: um.isRemembered, + + getId: um.getId, + getEmail: um.getEmail, + getStatus: um.getStatus, + getVerified: um.getVerified, + getUser: um.getUser, + getRoleNames: um.getRoleNames, + getStatusName: um.getStatusName, + + userRepository: um.userRepository, + userConfirmationRepository: um.userConfirmationRepository, + userResetRepository: um.userResetRepository, + userRememberRepository: um.userRememberRepository, + + onLoginSuccessful, + + getById: um.getById, + getByEmail: um.getByEmail, + getByUsername: um.getByUsername, + }; +}; + +export const createAuthAdmin = ({ + req, + res, + datasource, + auth, +}: ReqResDatasource & { auth: Awaited> }) => { + const loginAsUser = async (user: User) => { + await auth.onLoginSuccessful(user, false); + }; + + const loginAsUserBy = async (identifier: { + id?: number; + email?: string; + username?: string; + }) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth + .userRepository() + .findOne({ where: { id: identifier.id } }); + } else if (identifier.email !== undefined) { + user = await auth + .userRepository() + .findOne({ where: { email: identifier.email } }); + } else if (identifier.username !== undefined) { + user = await auth + .userRepository() + .findOne({ where: { username: identifier.username } }); + } + + if (!user) { + throw new UserNotFoundError(); + } + + await loginAsUser(user); + }; + + const createUserInternal = async ( + requireUniqueUsername: boolean, + credentials: { email: string; password: string; username?: string }, + callback?: TokenCallback, + ) => { + validateEmail(credentials.email); + validatePassword(credentials.password); + + if (credentials.username) { + credentials.username = credentials.username.trim(); + } + + if (requireUniqueUsername) { + if (!credentials.username) { + throw new InvalidUsernameError(); + } + + const occurrences = await auth.userRepository().count({ + where: { username: credentials.username }, + }); + + if (occurrences > 0) { + throw new UsernameTakenError(); + } + } + + const hashed = hash.encode(credentials.password); + const verified = Boolean(callback); + + const user = await auth.userRepository().insert({ + email: credentials.email, + password: hashed, + username: credentials.username, + verified, + status: AuthStatus.Normal, + resettable: true, + rolemask: 0, + registered: new Date(), + lastLogin: null, + forceLogout: 0, + }); + }; + + /** + * Creates a new user with the provided credentials. + * + * @throws {InvalidEmailError} When the provided email is invalid. + * @throws {InvalidPasswordError} When the provided password is invalid. + * @throws {EmailTakenError} When the provided email is already in use. + */ + const createUser = async ( + credentials: { + email: string; + password: string; + username?: string; + }, + callback?: TokenCallback, + ) => { + return createUserInternal(false, credentials, callback); + }; + + /** + * Creates a new user with a unique username. + * + * - Ensures the username is unique before creating the user. + * + * @throws {InvalidEmailError} When the provided email is invalid. + * @throws {InvalidPasswordError} When the provided password is invalid. + * @throws {InvalidUsernameError} When the provided username is invalid. + * @throws {UsernameTakenError} When the provided username is already in use. + * @throws {EmailTakenError} When the provided email is already in use. + */ + const createUserWithUniqueUsername = async ( + credentials: { + email: string; + password: string; + username: string; + }, + callback?: TokenCallback, + ) => { + return createUserInternal(true, credentials, callback); + }; + + const addRoleForUser = async (user: User, role: number) => { + const rolemask = user.rolemask | role; + await auth.userRepository().update(user.id, { rolemask }); + }; + + /** + * Adds a role for a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const addRoleForUserBy = async ( + identifier: { id?: number; email?: string; username?: string }, + role: number, + ) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return addRoleForUser(user, role); + }; + + const removeRoleForUser = async (user: User, role: number) => { + const rolemask = user.rolemask & ~role; + await auth.userRepository().update(user.id, { rolemask }); + }; + + /** + * Removes a role for a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const removeRoleForUserBy = async ( + identifier: { id?: number; email?: string; username?: string }, + role: number, + ) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return removeRoleForUser(user, role); + }; + + /** + * Retrieves the roles for a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const getRolesForUserBy = async (identifier: { + id?: number; + email?: string; + username?: string; + }) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return auth.getRoleNames(user.rolemask); + }; + + /** + * Checks if a user identified by id, email, or username has a specific role. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const hasRoleForUserBy = async ( + identifier: { id?: number; email?: string; username?: string }, + role: number, + ) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return (user.rolemask & role) === role; + }; + + const deleteUser = async (user: User) => { + await auth.userResetRepository().delete({ user }); + await auth.userRememberRepository().delete({ user }); + await auth.userConfirmationRepository().delete({ user }); + await auth.userRepository().delete(user); + }; + + /** + * Deletes a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const deleteUserBy = async (identifier: { + id?: number; + email?: string; + username?: string; + }) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return deleteUser(user); + }; + + const changePasswordForUser = async (user: User, password: string) => { + await auth + .userRepository() + .update(user.id, { password: hash.encode(password) }); + }; + + /** + * Changes the password for a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const changePasswordForUserBy = async ( + identifier: { id?: number; email?: string; username?: string }, + password: string, + ) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return changePasswordForUser(user, password); + }; + + /** + * Sets the status for a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const setStatusForUserBy = async ( + identifier: { id?: number; email?: string; username?: string }, + status: number, + ) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return auth.userRepository().update(user.id, { status }); + }; + + /** + * Initiates a password reset for the user, but only if that user + * has already verified their email address. + * + * - Ignores user.resettable (i.e., initiates reset regardless of this value). + * - Doesn't care about how many open requests there currently are for this user. + * + * @throws {EmailNotVerifiedError} When the user's email is not verified. + */ + const initiatePasswordResetForUser = async ( + user: User, + expiresAfter: string | number | null, + callback?: TokenCallback, + ) => { + if (!user.verified) { + throw new EmailNotVerifiedError(); + } + + expiresAfter = !expiresAfter ? ms("6h") : ms(expiresAfter); + const token = hash.encode(user.email); + const expires = new Date(Date.now() + ms(expiresAfter)); + await auth.userResetRepository().insert({ user, token, expires }); + + if (callback) { + callback(token); + } + }; + + /** + * Initiates a password reset for a user identified by id, email, or username. + * + * @throws {UserNotFoundError} When the user cannot be found by the provided identifier. + */ + const initiatePasswordResetForUserBy = async ( + identifier: { id?: number; email?: string; username?: string }, + expiresAfter: string | number | null, + callback?: TokenCallback, + ) => { + let user: User | null = null; + + if (identifier.id !== undefined) { + user = await auth.getById(identifier.id); + } else if (identifier.email !== undefined) { + user = await auth.getByEmail(identifier.email); + } else if (identifier.username !== undefined) { + user = await auth.getByUsername(identifier.username); + } + + if (!user) { + throw new UserNotFoundError(); + } + + return initiatePasswordResetForUser(user, expiresAfter, callback); + }; + + return { + loginAsUserBy, + createUser, + createUserWithUniqueUsername, + deleteUserBy, + getRolesForUserBy, + addRoleForUserBy, + removeRoleForUserBy, + hasRoleForUserBy, + changePasswordForUserBy, + setStatusForUserBy, + initiatePasswordResetForUserBy, + }; +}; + +export const middleware = ({ datasource }: { datasource: DataSource }) => { + return async (req: AuthenticatedRequest, res, next) => { + ensureRequiredMiddlewares(req.app); + req.auth = await createAuth({ req, res, datasource }); + req.authAdmin = createAuthAdmin({ req, res, datasource, auth: req.auth }); + await req.auth.processRememberDirective(); + next(); + }; +}; diff --git a/packages/express-session-auth/src/user-confirmation.entity.ts b/packages/express-session-auth/src/user-confirmation.entity.ts new file mode 100644 index 0000000..b28f392 --- /dev/null +++ b/packages/express-session-auth/src/user-confirmation.entity.ts @@ -0,0 +1,32 @@ +import { + Column, + Entity, + Index, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { User } from "./user.entity.js"; + +@Entity("users_confirmations") +export class UserConfirmation { + @PrimaryGeneratedColumn("increment", { type: "int" }) + id: number; + + // Eagerly load this so we don't have to do this everywhere: + // relations: ["user"] + @OneToOne(() => User, { eager: true }) + @JoinColumn() + user: User; + + @Column({ type: "varchar", length: 200 }) + @Index() + token: string; + + @Column({ type: "varchar", length: 200 }) + @Index() + email: string; + + @Column({ type: "datetime" }) + expires: Date; +} diff --git a/packages/express-session-auth/src/user-remember.entity.ts b/packages/express-session-auth/src/user-remember.entity.ts new file mode 100644 index 0000000..4dd57a1 --- /dev/null +++ b/packages/express-session-auth/src/user-remember.entity.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + Index, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { User } from "./user.entity.js"; + +@Entity("users_remembers") +export class UserRemember { + @PrimaryGeneratedColumn("increment", { type: "int" }) + id: number; + + @OneToOne(() => User, { eager: true }) + @JoinColumn() + user: User; + + @Column({ type: "varchar", length: 200 }) + @Index() + token: string; + + @Column({ type: "datetime" }) + expires: Date; +} diff --git a/packages/express-session-auth/src/user-reset.entity.ts b/packages/express-session-auth/src/user-reset.entity.ts new file mode 100644 index 0000000..78b8643 --- /dev/null +++ b/packages/express-session-auth/src/user-reset.entity.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + Index, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { User } from "./user.entity.js"; + +@Entity("users_resets") +export class UserReset { + @PrimaryGeneratedColumn("increment", { type: "int" }) + id: number; + + @OneToOne(() => User, { eager: true }) + @JoinColumn() + user: User; + + @Column({ type: "varchar", length: 200 }) + @Index() + token: string; + + @Column({ type: "datetime" }) + expires: Date; +} diff --git a/packages/express-session-auth/src/user.entity.ts b/packages/express-session-auth/src/user.entity.ts new file mode 100644 index 0000000..2e7ef06 --- /dev/null +++ b/packages/express-session-auth/src/user.entity.ts @@ -0,0 +1,106 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from "typeorm"; + +export const AuthStatus = { + Normal: 0, + Archived: 1, + Banned: 2, + Locked: 3, + PendingReview: 4, + Suspended: 5, +} as const; + +export const AuthRole = { + Admin: 1, + Author: 2, + Collaborator: 4, + Consultant: 8, + Consumer: 16, + Contributor: 32, + Coordinator: 64, + Creator: 128, + Developer: 256, + Director: 512, + Editor: 1024, + Employee: 2048, + Maintainer: 4096, + Manager: 8192, + Moderator: 16384, + Publisher: 32768, + Reviewer: 65536, + Subscriber: 131072, + SuperAdmin: 262144, + SuperEditor: 524288, + SuperModerator: 1048576, + Translator: 2097152, + // XX: 4194304, + // XX: 8388608, + // XX: 16777216, + // XX: 33554432, + // XX: 67108864, + // XX: 134217728, + // XX: 268435456, + // XX: 536870912, +} as const; + +const createMapFromEnum = (enumObj: Record) => { + return Object.fromEntries( + Object.entries(enumObj).map(([key, value]) => [value, key]), + ); +}; + +export const getStatusMap = () => createMapFromEnum(AuthStatus); +export const getRoleMap = () => createMapFromEnum(AuthRole); + +@Entity("users") +export class User { + @PrimaryGeneratedColumn("increment", { type: "int" }) + id: number; + + @Column({ type: "varchar", length: 50, nullable: true }) + @Index() + username: string; + + @Column({ type: "varchar", length: 100, unique: true }) + email: string; + + @Column({ type: "varchar", length: 1000 }) + password: string; + + @Column({ type: "int", default: AuthStatus.Normal }) + status: number; + + @Column({ type: "boolean", default: false }) + verified: boolean; + + @Column({ type: "boolean", default: true }) + resettable: boolean; + + @Column({ type: "int", default: 0 }) + rolemask: number; + + @Column({ type: "datetime" }) + registered: Date; + + @Column({ type: "datetime", nullable: true }) + lastLogin: Date; + + @Column({ type: "int", default: 0 }) + forceLogout: number; + + @CreateDateColumn({ type: "datetime" }) + createdAt: Date; + + @UpdateDateColumn({ type: "datetime" }) + updatedAt: Date; + + @DeleteDateColumn({ type: "datetime" }) + deletedAt: Date; +} diff --git a/packages/express-session-auth/src/util.ts b/packages/express-session-auth/src/util.ts new file mode 100644 index 0000000..613fbe5 --- /dev/null +++ b/packages/express-session-auth/src/util.ts @@ -0,0 +1,68 @@ +import ms from "@prsm/ms"; +import cookieParser from "cookie-parser"; +import { randomBytes } from "crypto"; +import express from "express"; +import session, { MemoryStore } from "express-session"; + +const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export const isValidEmail = (email: string) => emailRegex.test(email); + +// const isMiddlewareUsed = (app: express.Application, name: string) => +// !!app._router.stack.filter( +// (layer: { handle: { name: string } }) => +// layer && layer.handle && layer.handle.name === name, +// ).length; + +// export const ensureRequiredMiddlewares = (app: express.Application) => { +// const requiredMiddlewares = [ +// { +// name: "cookieParser", +// handler: () => cookieParser(), +// }, +// { +// name: "session", +// handler: () => +// session({ +// store: new MemoryStore({ captureRejections: true }), +// name: "pine", +// secret: randomBytes(32).toString("hex"), +// resave: false, +// saveUninitialized: true, +// cookie: { +// secure: process.env.NODE_ENV === "production", +// maxAge: ms("30m"), +// httpOnly: !(process.env.NODE_ENV === "production"), +// sameSite: "lax", +// }, +// }), +// }, +// ]; + +// for (const { name, handler } of requiredMiddlewares) { +// if (!isMiddlewareUsed(app, name)) { +// console.warn( +// `Required middleware '${name}' not found. It will automatically be used and you may not agree with the default configuration.`, +// ); +// app.use(handler()); +// } +// } +// }; + +const isMiddlewareUsed = (app: express.Application, name: string) => + !!app._router.stack.filter( + (layer: { handle: { name: string } }) => + layer && layer.handle && layer.handle.name === name, + ).length; + +export const ensureRequiredMiddlewares = (app: express.Application) => { + const requiredMiddlewares = ["cookieParser", "session"]; + + for (const name of requiredMiddlewares) { + if (!isMiddlewareUsed(app, name)) { + throw new Error( + `Required middleware '${name}' not found. Please ensure it is added to your express application.` + ); + } + } +}; diff --git a/packages/express-session-auth/tsconfig.json b/packages/express-session-auth/tsconfig.json new file mode 100644 index 0000000..a525b77 --- /dev/null +++ b/packages/express-session-auth/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ESNext" + }, + // "include": ["src", "express-session-auth.d.ts"] +} diff --git a/packages/express-session-auth/tsup.config.ts b/packages/express-session-auth/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/express-session-auth/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +}); diff --git a/packages/hash/.npmignore b/packages/hash/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/hash/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/hash/README.md b/packages/hash/README.md new file mode 100644 index 0000000..8d878b9 --- /dev/null +++ b/packages/hash/README.md @@ -0,0 +1,43 @@ +# hash + +A very simple string hashing library on top of `node:crypto`. + +# Installation + +`npm install @prsm/hash` + +## create + +```typescript +import hash from "@prsm/hash"; + +hash.create("an unencrypted string"); +hash.create("an unencrypted string"); +hash.create("an unencrypted string"); + +// sha256:UfH7lmEc5dr65iFPmvsKthzAgMHtdV6Qb4FXYSqlnOaQoZmqQWLBrPnJGLZmQontirQZKO9nTIz+zs544n0x7Q==:6qG75Cp5hysNWs+8TO65fzc1FaSZxykaWa3iatPrw4s= +// sha256:Wq6vrcGG4mKlM7r8DAuDHcYxJlG8fOEoO2sNWofl/snmsZPTaBuy8Dg6i2J28TdcncSgK8EhrCqgv69h5Kk2xA==:QvAc6op8ScJex38AYrZUtFDd69c4OJv5SsVIRgR+FPw= +// sha256:e16qmZpJiy1qvGycPkJz0qQnCdyAguGAFV8rqCokiFml10nl9lVU1v0hZ6QBy+laI0AYkHsYtt6wMkEOuNhpMw==:L3bHZeriSAjy8wEIz/fURxhOqxa8KltuvpHPE/nE/eQ= +``` + +## verify + +```typescript +import hash from "@prsm/hash"; + +const valid = hash.verify( + "sha256:0SA+O819D52jZOqWuzIWa+KLyT+Ck+b5ze4HI7fAJOhRW3FYk527GnuVOS/pricLy1KqwUfk5wWyQx4z5x3fsA==:wPs8DRMOrZEJYeaPxZzccGPJSozGvNqRhhS6f8ITOyM=", + "an unencrypted string", +); +// valid = true +``` + +## custom hasher + +```typescript +import { Hasher } from "@prsm/hash"; + +const hash = new Hasher("sha512", 128); +// hash.create(".."); +// hash.verify("..", "sha512:...") +``` diff --git a/packages/hash/bump.config.ts b/packages/hash/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/hash/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/hash/bun.lockb b/packages/hash/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..94c73a64e44c35854cf6247091c1ff56c3ac53ce GIT binary patch literal 77155 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 AuK)l5 literal 0 HcmV?d00001 diff --git a/packages/ids/package.json b/packages/ids/package.json new file mode 100644 index 0000000..bede136 --- /dev/null +++ b/packages/ids/package.json @@ -0,0 +1,30 @@ +{ + "name": "@prsm/ids", + "version": "1.1.1", + "description": "", + "main": "./dist/index.js", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "test": "bun tests/index.ts", + "release": "bumpp package.json && npm publish --access public" + }, + "author": "nvms", + "license": "Apache-2.0", + "dependencies": { + "long": "^5.2.3" + }, + "devDependencies": { + "bumpp": "^9.5.1", + "manten": "^0.6.0", + "tsup": "^8.2.4", + "typescript": "^4.9.5" + } +} diff --git a/packages/ids/src/index.ts b/packages/ids/src/index.ts new file mode 100644 index 0000000..9dbe743 --- /dev/null +++ b/packages/ids/src/index.ts @@ -0,0 +1,74 @@ +import long from "long"; + +export default class ID { + private static MAX_INT32 = 2_147_483_647; + private static MULTIPLIER = 4_294_967_296; + + static alphabet: string = + "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ-_"; + static prime: number = 1_125_812_041; + static inverse: number = 348_986_105; + static random: number = 998_048_641; + + static get base(): number { + return ID.alphabet.length; + } + + private static shorten(id: number): string { + let result = ""; + + while (id > 0) { + result = ID.alphabet[id % ID.base] + result; + id = Math.floor(id / ID.base); + } + + return result; + } + + private static unshorten(str: string): number { + let result = 0; + + for (let i = 0; i < str.length; i++) { + result = result * ID.base + ID.alphabet.indexOf(str[i]); + } + + return result; + } + + static encode = (num: number): string => { + if (num > ID.MAX_INT32) { + throw new Error( + `Number (${num}) is too large to encode. MAX_INT32 is ${ID.MAX_INT32}`, + ); + } + + const n: long = long.fromInt(num); + + return ID.shorten( + n + .multiply(ID.prime) + .and(long.fromInt(ID.MAX_INT32)) + .xor(ID.random) + .toInt(), + ); + }; + + static decode = (str: string): number => { + const n: long = long.fromInt(ID.unshorten(str)); + + return n + .xor(ID.random) + .multiply(ID.inverse) + .and(long.fromInt(ID.MAX_INT32)) + .toInt(); + }; + + static randomizeAlphabet(): void { + const array = ID.alphabet.split(''); + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + ID.alphabet = array.join(''); + } +} diff --git a/packages/ids/tests/index.ts b/packages/ids/tests/index.ts new file mode 100644 index 0000000..85e1fda --- /dev/null +++ b/packages/ids/tests/index.ts @@ -0,0 +1,36 @@ +import { describe, expect } from "manten"; +import ID from "../src"; + +describe("ids", async ({ test }) => { + test("encodes as expected", () => { + const encoded = ID.encode(12389125); + expect(encoded).toBe("7rYTs_"); + }); + + test("decodes as expected", () => { + const decoded = ID.decode("7rYTs_"); + expect(decoded).toBe(12389125); + }); + + test("changing the alphabet is effective", () => { + ID.alphabet = "GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT"; + expect(ID.encode(12389125)).toBe("phsV8T"); + expect(ID.decode("phsV8T")).toBe(12389125); + }); + + test("shuffling the alphabet still allows you to decode things", () => { + ID.randomizeAlphabet(); + const encoded = ID.encode(12389125); + const decoded = ID.decode(encoded); + expect(decoded).toBe(12389125); + + console.log(ID.alphabet); + + ID.randomizeAlphabet(); + // const encoded2 = ID.encode(12389125); + const decoded2 = ID.decode(encoded); + expect(decoded2).toBe(12389125); + + // expect(encoded).not.toBe(encoded2); + }) +}); diff --git a/packages/ids/tsconfig.json b/packages/ids/tsconfig.json new file mode 100644 index 0000000..7fc5833 --- /dev/null +++ b/packages/ids/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "esnext", + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist" + } +} diff --git a/packages/ids/tsup.config.ts b/packages/ids/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/ids/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +}); diff --git a/packages/jwt/.npmignore b/packages/jwt/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/jwt/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/jwt/README.md b/packages/jwt/README.md new file mode 100644 index 0000000..b37beb3 --- /dev/null +++ b/packages/jwt/README.md @@ -0,0 +1,43 @@ +# jwt + +A package for encoding, decoding, and verifying JWTs. + +# Installation + +`npm install @prsm/jwt` + +## Encoding + +```typescript +import { encode } from "@prsm/jwt"; + +const payload = { + iat: Date.now(), + exp: Date.now() + 3600, +}; + +const token = encode(payload, process.env.SIGNING_KEY); +``` + +## Verifying + +```typescript +import { verify } from "@prsm/jwt"; + +const result = verify(token, process.env.SIGNING_KEY); + +if (!result.sig) throw new Error("signature verification failed"); +if (result.exp) throw new Error("token has expired"); +if (!result.nbf) throw new Error("token is not yet valid") + +// token payload is available at result.decoded.payload +``` + +## Decoding + +```typescript +import { decode } from "@prsm/jwt"; + +const result = decode(token); +// { header: { alg: "HS256", typ: "JWT" }, payload: { iat: 123456789, exp: 123456789 }, signature: "..." +``` diff --git a/packages/jwt/bump.config.ts b/packages/jwt/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/jwt/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/jwt/bun.lockb b/packages/jwt/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..713968ed305988c975078ccd8d90228e910564a4 GIT binary patch literal 77893 zcmeGFc{o$=$G;w#>3l3g8M^jr1YddpZ3r81wV>jMQ0()^V7>va|>$sQmxr_z@BQdUmL^SJX zvJcRT;)~uqrq_M-ZcA|~|1Qu9gL$|46N4f6U)pcGe4u&z!=~DU!T2$NPX~bZ1DX(M za-jDD?dIz41U_Oa0Woan>Tc>{>}rn52kwA2eoodd#%|`W7<(_+3WK@1dkgPoW$s{Y z;fZnEE?)(j2((WEI8vZ3?Ho;Ddlg&^h7yz|f!+`FKD;gbu5ImMWo>E&+F84rUp5Ci z8q|}3b{66eJXjY)Dfo23+ zX}g{SXckb$1DYA=F_1imfPS^DbAX2Uux&;w*0*TG|a28?e=EYE}KM}ASlp|wWWij3rJj7J7ZUv#~_PMje)iV!Gk<{K*Rnk z0}cBxv#pWq;3OzRfB1ohe2xz0Zr1kZ7<+4bYg1ke2Xj{wcWXN{Ox5A7aeoLj_vSr=${y~zU&+X-!-XWTx&cl-Pj`xd?*Xqa!Wfrhv;pdl`8`}{4SVZM29 zmo0#X?KFUf?IeJPdCIkYo_zZ}X1hGYwuLt{H*vQFaj|y=KY)P2c6GEf#+X_;+FF|# zySSJen}OImS@Ca;qu#bYF0eJ97&~}^dD7I%)Yx_fKIM}%IdK%l?VT@fYos3;v&COj+jqQxh z%uSB~01&LuR(`A=-5M7-4}oFE*xT8G?8IQ)EgYf37@In}fV74Eu`qGGj4^X`1M@rv z<7l;&50=)J=5FA;n=!PG!FV6v%4=l)@-()yvvzd@C)A+|>`jjK*cY%@?`NiQ#ZfiVMC^|6gEE| z?m%;s=%%L7ygls8T!{s@(!K1ilr=aJ-aSX*GO1>IMKz4W_v*}7PccQxly>#^fpa?U zvbaCYMka?t1m+j=CbEs5g=Z{XHq#(}Nu(aqRluO5xYw)u(Ehikyq*@?;WIS4%BOPn zb^Y3q?G&OWBeTDvn*r=$;WJ6!Q@`IyFwTt0*|WT-YX z%lr2HoT`s7YDLEe>omarIk+3iTX-O(H^MPF~#cTxL=o^ zd61=U4|9dyuB2}HA1O}jcDo9lnB_j_IUXson=wtEGSk!Q6|0j|=C4nqZhB5`;>;>8 zMM!e8O(&;S;d{#HKk?|kJgz>exH9#>dAf*&9mpt@6z)=#PtpWd*{+b9+x`-2PVk&7 z{n#y8<$F=Vl#n?@>+|5R<^m2OqWF=iN}4*x^)^!KhPZ>RC%e+Q`oi25I2A=K20td0 z2FP+sn1q}l#x-bp#4MVi`S_WtUH2K14C&i9T<#w3y`exvGSqg2XYI`zy_d?aQSVMa zNqjX6mm9CmH2Z4r2YpE57%#A(?L!7GjvjqK9%O<@>2MB~zR-TtuVNDV*(^y^%O2IF%kpLqG5Lo=!V^7TjaOq=O@d&oJ>!&`iG zb?GmDkEV0ddZ#m{?Q%8aPVy8k<3;I9+Jf}q-pWmH2qxGSm7Cro*8#^|->fUuxAHD! z`K+&zgF@OOxkllnyW$=h$gvQ{B~9*U5tsGAUnoC*_HEhys1h#%QVH%SyJFtHTJk&O zovs!AZ1i^a0jkqKqD7}zY{C<72$K4oAFNb2xq;2IG9neZJa)%z{=VeWQxnRyJtT+7 zm+UU4ZFJqPkme%eIJzrr_b;6F5>tRB{u^1(zBHWDB`EmRDzP{Rfxq{Wg5&jsVL7JwQ=_M~ZOFu-O>5=rgw-Wg5xMf|a0j(r| zCG(*;YI_KZ3DEi)RXS6J_se~fM1uZ< zzkYoGs~dc{yAw4tDcRc$-X@H{{p3NRFnXRSigmc+z>yI>qsN|UU%j~9E-VCVi(kKK zP@&XRq8?Q+?&xA2@hOaCYj)4q&*@(_>Ri9u`{Hnba+UUcvze)- zr5Sl1VcA)^Uwr;gS#_BQs3>dMKa&yiGUAL5E$NnD`Ss~Xq*-O^$Q(_*(NGM>5$`LH zFGO9v(mA8(sL1BkjT87|yk3`-f~_!HB*DT!rY-vGbX<(>4?7%W{+f>~?4t2znopWJ z`E-}oBnMJoyJ}H+sU{^?vf+cX)NbCi3k~tt*d$uV{wcqZ`SPFgD%iO|>P=;!&JXc4 zl>$dzf`Idf<)4r3Zw^W7(#D?OSI1-C5XsRX_sef-qS2Irg;CF+TU~9W$LGaJO2Q`3)KNFz|ppI}yY_73er(AL4f^ z2wn@|Wwzr7ZU3(QIDl8&?my&2#^Im*Kb@%(g>ak3_6TI?DtM%2e}cv);1o=y`7FBg0BL2a3`~wzi3|g`R@e5 z&-^d^-(k?_5IiGrTpnc~-RAE;BY0PUmjU>l_8Z|u@TCBMavT3|@^>2GB>^5{;oQH| zF+}XEfQc8}8vT{OJM9O84+VJWKX7ByHd2rD^S^1NT|L0V`5WGsLHXV5hY$eKY&U>_;}Zu~R>9*!Tx!2|lS z6G80V0(d0@KT`vF#n+ch+ojg-wD$07Qo8^ zJZy{9{T&WDhqQeU@GyVCG_Z+B>XCN;O(X5dNVf3s`h#^l9Rmcf3h;3J;J8EV@8UxN z9`2t2bo1Inu47pKJ3;Kz@5f-&0Up}jDHie~ct?PT_dl=>Uc=Pz!wv+&cLF^0A7Y^o zNc}%iAngv3ZmmBE_IKlF4)C(u{-fs*6LU9XK5%1HeQUD8mlM14zx_e%7XduX|9^Y^ z5RqXpX8|6LKNN&_tMuQ0BJFem9=N$Vez5iL_CEyR;r;>gA!G30t^Us$#QrmYm)Z9J zch)cjPX=BN9NWf&_X9f?1TO+W`hS3r1$f;*z|Z`leFaJkM&l3cCjq>{AK+I2-ue&l zCg7!z@gLxy1N@)(&v+2LL;C~!!2oam2ly#~H~Is-5|}Xm1fK)&e-b}@nm@B|0q}ov z{nP@y_8-KLlJ@_9{(^JZP6U}hlmQ;zeco94QO(X5}03NPiNdFQ2-*8Ahg1-as;1%d!>-X>Cn*jb8z=PX}9j{%` z2gLpX`mOyJEbr7d8m|WMaQ_A42M>f7w)s0j+SLI(j32Uo?{p1B@VE?H_F)+r!@skO zoI~)E0FT5Uo`W1a5d`l8@X&u`|FY8#aD+4_2 zKeP+S;CJm`2Y88X{~;FPMeO`HjkK!+_~QVNjQ{V(AH%p6f5->z|E~XH051vbBfg{e zuz>Er3gUkRBL)M$y0D1{TiM^X{|w+^{}I2DH~^}@6~z7zMhxZ@z$5)f^5}2D;3R@S z!-T;|q5A(%IOIHnj{|u4{0fQR?~Y$Dz{B_>e81~I9rOP@e}uk4KXxLBJp+J;*UwJl zj&LLRIDm)O5A+-M9jQm!{Wp!Ydjs%-0Ke0?A$$maeH#z$!@mD+|GikY)=z{F;r}O% zw0{NgGQfXW-l=WKjo|mPVlYwwkHmkc90*=<8;_JB_f7<99}4hr{zBrv(|HuZzXEtT z|Nq=1Mp}5!2eW$m-_?!$RFD0;oN%u@o)Qo8Q|gk{crI#0RJcb z$Km?3_$vZDeE#!q{lD{v_WJ-H-hcdC`#ju#=Kp1Y|D*8(_&>>iJf1%re?@@*6Z=sB z|0m<$2k?IqKUUuVd4BwF<}U|;|C9cg0sNozABXSH>?;ENpIrYD01x*+Sa1QsH5ge( z;q~-)f~-Gv01t-nul)y7|94n;4rw<9@NoTvwqYHz4}fj{P7pjZ|Nrbicj`OjMevvY z5MKcBF#bFB8|f2be|{Se%l~HmKMfw9D{jaCcYHwX-v)TN|A1I%f2SM>{x!gZE^O|< z;NFcG96J#NzYOp&esKTvJA&X9j{ML2Z&^scA z-;Ey^Fb?w{^1*8m;RPT6R*-f!01x-Sh~K~4|4e{~{YU&p^xql>ClUJ}0a$7q53z_J zJH@kun|IiM*!P`cAva?G0>C5tXIQ^e4g?>wjYoV(>LJ(P3DWK@z{C89c7Nv?Ac7|c z$phD)o%)UVgy1g#JiLE~czFEo_}u|`IQ|G8;YIBHH;uG=1@Pb!-o(ReV5j{+@XG)X zmViw>%w3oVI}rrW4<5dPA=v!>#6AH0UHc{g5AzS&gjm>zod{w-9^m2g-~9jtegEC- zuNi>R@&DcNBLvPPc<9sb;*|j&#vjgI5dXXJ3kG<2|Nc8zVCJvuw*lah^#}UC(={B4 z|2n{fe||g78)zE}f0UlfeoB0pV?X({V zesCL)lo9@a(nxy>(0O?M!uBx!a2?!0Ke0`MSMf-Hvqg0 z!2jF$;|pUjQrr07@d2@a8sLEoemfnv|Ly-(fQRe%zm4BhfJff{Aly6Ai2sWK53iq{ zUUvvLg69Ut|D^w>01xvA^1|HNsUL{_Sb&H7Uj)0;aX|3>01xvY+Jzjyi>CoEU%?h| z(>}$&@Lvhw;rt8zf%XxzM{FbYJH@vGJj`E80Qj9gAa=;bF&Jrphxu~=9LRP2 zPxj$?IA_5F`i|s1T9bo%8gM{?8tzS)U;zkP!?>}61M;wKYj&Wa{3i`YJ^$ zH1t<z}# z_<#cn)Uf})+xqIZ_5&J<-{#LP4clD<2h`WM>!F5x0pNi3f#84wHLMQ?2duvV4k&01 z>q9nc|FedBo2c#cXbo}E+x35^VY`^^c2FM#N8Wb%|H~ToD}TEmXiWg>O2Glomu;Vi z8kWnq%TU8{sn{+<4a-lr%TU9(zt}EA4bQ*aF8?PD`5U*-qcstzd%s-|HN5UVY?qSD>Ll4gH;l1t9-PL;M0bU_aNu0R^og@A_u#f7bB) zukG_t!}7*<`9Eps&o0<#Tf=_iZkPX)hNtkh&qEBnZsD<64=aGcK0tw*9320*?ri0k zJPZmDw1#o}zjbFbEdP(JXD}E4(|Q9fzTA3Yy)9#ORFIkDt)KhrZ2kS37VvqGGoKd_m#}L65+-rZJ+~oWzlk@ST zYY&{InQf;Z$13OwE46XGH+q;+Vl}H0@0mImO)Ts2T;;@||HP9q6Z_aFtJe)}986ZO z8=!dM-USh?;ODZXdVh>O$@D{0c|sMDm|5YaZ>Ox)Cpy~R5%eAyC^;SU)o3oJiL~AG zWut(4Gq!vW=1{zFjz$E_FPodm z&qWg>zp-GjeATT=)whpntYX?#fhkjH_+uyZT1|7%9&YPaav$Y5K}5dhbm$7{*bFr1CQNrd(_5yE#u|iry`fas$tr7u32T{EE2r1B5)1}<%ieCj)KUkSFzVoqa ze(3PwEc<25IiguSqC|aWx4bx~YC+7IkromTqfB;Z;m=1Se#nlAA3mX8-F?)JtGQYipgKv7Rl$7t~N#A zX2$OMV}l;w|6X2{cP&`bXbteAJ+jh157vqwSe&SIWqQUZy>SuGfh)^9ka}<6 z;rfHYBp)*lw#v@k>!}l>4!?7uOA;@A<%yvZigzEHcUtKS=fm^zy-wE!7vzR!Gq zLynp}I5lJNk(r}eL<)aCZN;ctqvM{*T;qp^jt;!#e8JS^%o*z^8TOge>wiS?!g~iq zuzB$ZWIV}}eC()51XVKBHN2nqHDo@`r49&VFm2FYHgo9t6*#3|!`eD-u);=m{tX*v zPmk7qdhI99%Eplc7t~R_#0V+S*jY}O*ss*G3!U8`2loqms4b6A#IE{rg*>`_MJ#91 zwRd)Ut-Zl8<-2XTD{sFb_38;!bTy;WwU=r#*Ug ziUGyDAIvF-4WAlvZ=B^4XvEkhfoo}PV4@&LqclWv+uAQ~N^>Z$-pG8e>h*EVV%H+;^89P59b+EdYqbT%juh6R@I}|Sk zLJBli&igE1ih0ugm-N&Fi@S{FSZFWeQ)K#97nudQ}iw%y0AO+HxU_1X}Nd^K8f&!;z|t(*I~hm%|F ziKpiy!?)~C=Zw!?Ja2q8!h1hL?2&9=Zk&^V0znK*!343@g!AP>s(9Tmy^7}8$%L~a7*FEv66G}fSi=(%BoQY)vFx=*Fa5E~yyp4FG> zq;Kj~GzrgcJ#i~0;Wl2Z+)I>~WZc?gCYo~*`{DVu21C3Y-4nd}tBZ6fUgSO+(qma$ zkD51p-P?Cu(`3d}{HXW7=iY>`bshYCLS%9G`D@>*j|tEGQpe6cd_(l9@S?|*d_!>7 zxd)iUHtUWyYEdr*6fZ5>-|XRHCGB13EN}-%*L<`i_Z^5>;<6WXjv-AVa{aMS&#m*b zyOL$8)iGld26?*XTS6J1G1H&Ct~^%ynqq16zRVWIONZvAP5*B0EB12l(Z(9@8AS%8 zE}oBFg5%bYHWt>Fp7CDk^rH7=&<5T_cEE*BtF)z*xGO2?00cmarF*LKS1%) zqj?RT2&zdKj!xiAWDqvy@yYN|6nRgDeelqy>+I1xfA-?B?}lYvXXzd4=*Tk$IHf=7 zy{$=zNaNSXSD^?X=gCLs83UTPFCbZU`oIyxrvbfE38hoqHxnN!Pe(9~I+DwpO`dMe zs9ruCH&`NN?So07bt|X8SrB8bkEM~sVPj(26BsBgiSqXln)jr&u1wO>>!tgp;W?t^fgt%?$(GgXA z6fYy1cU8+Qy!E>j*}MB~xooF%Y#y&^;_yoiymBP)F^pbO;>f#N#vwzvhhyPXT27dZ zevKB%v=%qh5r&M^g~M|y_asofOlV%wS3TjGA8fQEXj@x521pGoq|dFq8#I;56-{F; z7>$d-+$9aIoxgN6;9cawkNv{W_N+d;aAC&9-u_un;OoaW`%%2iXkKHr8oX z#Nm(Q$MLD!>tBC=-Dp^3c`|F{cxQ{N#25EWqwuQ~1}|0(7*@hHM??(R@dj{toBA8V z3K!AW8w;A(^eTloQ%{bx?cUp#vfVQZo;j6YSMIxprdvi&+|PaFb%OexRP6amR;u@B z2C^JUgUpO-+d~Paa^6+Q6_=jYgulZJSvOhHyg5UvhMMiFCQCe5)z!SjvuIxRyz}Wxt^q%`{oG4x{G_TuKO%LIn-u%L2Wv7{k zsWVP6-n0$VSiQ!-de`~3kMO1E4;uRiW(ChDOJ6;mqVy)b1BScLNWj9b9+#{JZRp~f@Cdqks-JJ=P0>8=F0h7UaC-jL#Cr~m=J&P z<}3QUOfPk)6W@JfYN1FKIuPUds#={Jd%t9y>r?VQuG%MN==)e+G;d0@*jQYXqgkGJ zJu|r;MIPofEC2oyZ;Hl=V4d|=C&g>@H;H#~$3PWE3v zGsw?Yc4zQL;9$V1V%704qOZm7+PFO=x}>+j*QvUp6My2M?O0K6FwG3}qz;N#;4fC- zFTc@ltGiSR3ddK&AJ)DOD~^AyVr4@qEjJ`yS~rnuub5|Pr8H}5r>4C2g+$G8?}=jh zbJuh)e>xeQb;(|2qq7Oc3*VO_g0<^gpWkI9ef3coQQIunOI@0*yxweiX;$@pG z-|usYSEA!^6wS*bxMp!}HzWD^1Hv-S2bHb3ADXgwUTfdGLY!i!twKy3{LuG7rVMXN zVFPn)HSfL;w5k0|b)w5dS-FZC@8%NG^NtXj_xk#$Q)y%x9>Zb;L`6@o$=|;tRg?4N zg4>`!iT>L2Iak8R=av%pU$rTI zlHGTE@Jb%E`ejU-7^8TPqy4ofBlBZsk+hdAEsYX4#fByC*Zi(~fSQlwlLG%JL4!>G zGj*v2+*0MT#hY)A$!U$hXVf^BvJ!NwT4p6w_cJFqiuVMX_w*~mQ^yUP?wx3|d$*A` z6t`YgVi|Pcq;pq(`s}$TYBMPoErY6gDmRDknsLX|cMJ5iim;}w1d%#))oZuD9$Q24 zo<#F@bEZxPNN0TMv*6cM?PipHryopz?A(iy`1dc9I;!S5qZ3L}--Ps6iVSEbMwGNO2e(Y(4!PvwbZ3;adz6TZ}4NWy^sze z4(bW>v#w6G7(neqFi!q2T zU-k>S%60pdduNPpl9J;Aab3Hn?(jnb>;hS;|Zue-&0#X0{4w0hTd_`4> zUy3MRQ8cf+h*jO0o><~0e+G3oYOd4Aj7iC<(l#XH+UyrQ&n~W{w%PBMX45mcsxicu zU&qp1dL!ZJz0nNTnB6$~r?-Cf8( z6r(Sv>4Cqx@18T)xdZkXAC`L+zi!?p)eEROWm>v;UwMr4{HfBPRg@@R@xNF>{?Z+b zqkWYpctz?psb_Wa9M>Sf2>CJ&wOaXlPqWa2D)S@Ze%G|f4-Pk&?DI}BmDFPPGt&=v zOTAk!>rh57g@7B1R|3sj6B+eOyAJ%G@0qeoGGTYa(u9NYoSHpinBJV>jq0Y0%&_Wq z3_5Tl%_r)80?+PPED`Z&-%?+G@1Yk3Cu5JZz~40w{t>`u1d?dpuGKGoCo~nlbW*#= zeO9NtMs@e$cQT)KCIJP)!|ds@?W$@;&u_`!Ed2gP#4gE|v63-mwCB77TSYED$xJH| z-`4uJ#Vdv8y<%lsrErRwAzgow2p6X|TIA-j2n*4frzUMo-v>lS4+t~HemrLt7w(&K z^G2l2gLJ%)I#g<0*V(;jyZc(Y&8Uafp(5WxgAG9eLw(Pr)=#6}l>aUM&$_LxpZqO1Oyb+1<_hoc;plb4U@_qh3E)R+C~YjGTM-KsOTdEv8E88q+o zXR^7$YkCShDo?O*Nn2=!P zF)U|$>flL|LmgUC1zWuEPhtG^{T4`%eVfd*d%F8ech%FPm2ZquEK=ep^hd7l9@Ooh zcw+voG{Fx4PDeDRS$`vBkIKnaw}bIHN@1h3^q+F$a0T`er{`|+`oX{R0Kd!imtf5v zd`>ZHt0J(~;B>-usxB|Q6zE~?xSHZMU6yYD@UdariKcPkxq`eiOwTVwC0wnK7ic^t z@H{+x^ub59JA`^$&y61`~ai;U#uVwwEfX! z>6RGcEQiD*_3Q(op%(X+4-3k9+v*UQI*R?gP`LyLvpj<1J;2L1q_$+ z$bIf#`Le9%S-%+voXt4E_p^v#J-?s9bRX@N{#0;D`-#$=Qu{c;=*@%Tw$H|xNpeG^ zZeF^};$d6Q{{DQ58QtKl@G-gMY|a<%6P+hiR}8GOy!%nSXAn}Lu^A?IJ_OA=ZF#9& z6+PK+%kdS6t*(xD-+hLEbj%{4?lE62y_n+(!O4!8iad;n2T2OuM&GO3rTt7HL`$?6 zsx-HG;YFu_=G8ss|Lw>4864m2TG1Hg*|d*8ikUyx(q`$=)u+&ZpdvM4`6l(N`omPi zzPV1@LrvYEYcMxvS=gI?Se(%F{gkz}Kf`Xu0em;|FTpx56=Y`ls!AKGQRfJ)CzCBs zUMtCNoNp9m@mcSEsL<)%AToT3sM&8&Vef}y)g;4Yr%SWN3A~O!yGm_{og3S_Ki=Y1 z`im9B!J>HZ^``^p>5{3BRS%kYXL`C}>Po(l0{AGa7V_EKNxmPX_ zAH|gzPknfIIkB95Hvb^IxrRg)lhezK6&6(H*XD#y1dbQfO@!4hZ@q5;S25r}hvv1_ zC+!vreb2yPawg%a;U~3t3pM<`5$9?nI~YFt4M(d5>lZy6y|==1c6f>TLU1;(+Unuz zyT{CW{Pk_96;9gT+V&SP!|yKtCD^wUO*&6@$M#RvkVTAqTYn%NHd!e+G9s)JFj(D1 z%@-dLARWK*`O*no_80kt9=%HWPrlZ4NF9(S#V42yjA?^@!hIj)MZTW~$+2TJyM3;v z@RxtyCH(oid1S_8`}#v|v!$5=O*wRCl_R{fRrCiLp5QLi>m=M;9@ak_neemk-0=_Q z&8bO8?@;y2f|ppxep2->ZvpQwdD0gb-{>D{!LO{Te;*^GW?R~*>D*-WX7tw*%dT{n zDygEuMmqxWu&9yayr(-8*F$u76>-$^O=`8e;}nFw+4dLS&#Iw$AKnn>SbyRsVRQSD z!f?5pwXR@m|J}jw?-$yeml(%xGC0{NF%w4GR@J#llMg*6|PCXK2xiHkw741uA63P4Y zZq@yck4|MB6jKDdmMjNCwI39t@4Ge7yk4Qcbprh9VVX_NxO&pBI0vdm_&0-)annNc_TSvK9`RPcY~g0QscQP?;k4tH<*{GQ%z~C`xD+|{OZaOSqI75; z-DVf9`ARHN>|ox2*VoEf-FZ>y5Q&I*JNh}-c{FbX8I{%4eAml07A`uQr*{;6G$O2@ zoqvaEq7q#2`tqXPJJFQVSL;0(X=B2ZH zoI3JRgYo9%8_g^C_gwe5Ocq>-iC8%DVN!GgFTztde*ePy3(t%V?)4X!^$JP~YbRPm z=)$G3lGyTtdauxVri13a@00svm+G^PmI6ukS>KPNS*7?q+?7w%mwRb4_ zS}`+4(T0Ld-2r={oNxEr(yIvQOU+fg@*Da~MN$6hqIr21HhOZn-}3Bg_z_c)ka#-t z<>%9e^z41D>-nb6N~e_hO`d#w7k_3D&q-ThSc87pYbg0^8Q*Opr87&?93m0xC|*4@ zFWb54%WlmP#>X|80$aX_k$Tq?UBG73C(f-B)Z#^zT$a+g&7jq5)rnbaWfNJ6lQVD& zv*+gH$1%Odc>WlmfOvjm5D2qk^w}sjG`;fuBxuyP0m#)e=neUO`bix;vUTNh}wjY zSLfvM!@Pa=bW8bO0wmO<9zi2g^g_<>7A;An^qfkWQ%YlP;VFX$xOy+{AL zcWGm%BtK(E#|2qD1IJx*IaIZ}2^=gZ$2Qzvk`OBI3Dc(Ce?s()N=w{P0>**b;U&qW zlC~&i5ftx5G%sd|Q2~>j$*fbiUMKJTcs15ic{&(3*wxN-d35Mo*Uuks>1WN#i#&Vn z2tQidKTrDcJ@+V!u-td$%Rb8!s$=iSv#s2>J@r64Ev&apWlql5yu3_KvXy>x=pp`;vxC?Ao3FhHpy_rs}sP9>}m@e{96cI{oMdwF$gp1Kh`ZTZC|>B{Mft3`>=t(v#?C0o~@g>!v=a(a-X|13B zni8}s)tFeCCdAIaL&d=Y&0CZI1TTO7?(G*c_Td~{KVCb13vZcKes&;m|5(&Lf9}uj zm1%9uM`j8fn}tP$I2;-;x{G=xDq4uYZOY^mp!)e4#cPS?{bES*I>vjv>}rgd?m31B z*|iw*b5X6=lmyl*cRiWnRo(rMt8Pgb(5qHm@iX~m*= ztt<@0yu_Qnqj;^+yz*yMB<>X#q}EB1EZzKQ+46w|NB`%bhcR>V=h=wM zyA%0Y3LT;k@=i6ZR`g(=dJnum!6|-@`<=U1=i$VhrIX7jUK=#8o?=&4+wFmjeh!PH zCo7_#n45H-3cN~qVzy%4xyAEW*H7ZfBC4DVvbSh&IP(OiPMCK8yq})?$;m&YE%PY8 zD8v2Up2WhK1cV^$K{>XeleV6`Xf75p(C?ba1Sk>`ulIe zMk4h*RN~k4^_sQBKRjt;TR2)*i{iCI^9I#v98(it8te*B;I5|NdBD*4;P0 z5enQ+jv>FZq+vr1~?@qbXwnz=U z^>aJQjQ)O!1Dbb!s_8TQw*YIySRUIVK<+C%f8=e@05T&At2v1EnzmaZFAE zvC4}@+CoR4k0b+%Ykr12YGqOFe63Z%_4+7Y7c{T+(~aygKJk?kY6tpBgcCH>n-ZtQ zZcCOVU7O`9N~Nq~_6@muDKN>6m^!E7);C#O@`vppgCK66F$ z);leksbs9M)c=|!YhUa;(7w2Omd~mwG z&OFe=XlZfJi2)&NRPc;si(jca?Vo9G%8YSn3&ToSK{*;VcpaEUot|Wu=k& zWi**zubRMr{T$cmZO&iAqnYvOb;twluSjM~bRt%IfO^in>-8sx?=GhAI%C~r_-#ya zoIML3%CjgdH2!MJ(_2>UTNdEZRD17bTGgs3>pNx_fAFnMkPFJ+%V=IL4cCqLiEU4X z&&W)tc&z4-$?W?U_{AR^7f1Z#dsL{G+!E!x^K1GZ$xC#;Qd}&nI+6rJGafYf&xy%R zqd8BLZQd7R2>bx2Cz==Qb&BFE+ijZ<`fMckiZWLz4{C+Z?B#svysD0yS=DmQCouX6 z?u+XbN{w2guIWJ{rpFGObO!{cTwh|MilqKrwCV5G^BpfVFYe2*Hr=1Hn8N(&kF$nj znwRu<6_8`kq`uXy)as^imwan~Z|&ucMWeQNx8wB_Qsq^}&(E((`TXc6nx);Hy7%5D z@78mID`?(<*;H``+x=8!lu;4d93iv2WyMGi(+bQS&L$cPA=|Bk6%O*6>fa-w>|J&$ z|58UEzr-lsB@vdWAK~5Hz5IS#?>8|7Qh?JN%_~Q)@p*4RoXlP+jK-kb@a!Aw=8&ve*RjMig5@2I3&m(%k+TYYN(sHvsP>=%ME z3^4~gM`%o9J95B<4A!O}s#!s}~A>?f9Z^|(cJy%sg) zGJN#4ysjzWk`amB>u-DXlh^2ICMVbZGbe-DzO_G-F29w#vh6RVyo%n@@EL-!(BIxNswb#_1DpE|qnY zjd`en{^f06_c%(jM75fIC5Pwc#4fsZd}b$K z{bBsoje$L^hov1oyI}h*-Jb!pz?1&yMuB&=H zj>*Qiyx`mmrsYrhyvrDf?8!7(w)PKO&uRV9ylxM?eQ|9(l-@IoIj}}VZoH3DnEitP zd3JX{&KaLPc2C{4fdrjtS@t#4dC{hGOXO~rJ-L>$FRv#OW;Rv`Uvx$J8-V6be(d`8 zg{XeuZJthIjW~0iy@_Ah3|VAX=S+gsBS!s=+k+m>H;x{>J*<8}F3N{H_ljK3^(#jt zR{YdwXP-(-^rCnJ(Y)I9;`~QAa0^_CFDDwKX5u*!^&;oxV^tVYLH?umAq zhKU}S&wW=ILHO3px2v0z5);?=NP>;0(3(L&rd43#&B+wj9?D}YcZ5H8+gmvIAkX_? zo&}?MH+s9T+CBF2I7sZmdnE8`t$|nbkDnT>!w;_Uk`lbf9ZWkP&F=H9S%Yz1wYOM% z)PtA8<5$RXL)WG8_TrR+m7z_4x1KBCK=bZ#A8Fb1%iW2j*!^0aN@$H&|DZ8W9hFh| zk9dY?25pLf*3%65xq~`p_lxtGTZ6FKFK#q<1a@9-U3<+w+V{zQE4~B-fHMTm8iprfdqLZ7v*v$8CwKO(#%8n7^s#*GxZ-0=N zwEx!o3+(3q-@A$CHF;5X#XbMJx2Rr$hk=ulvtdXeEjIU+nJc- zBMJ4=kH5UPP5RPb?{+d!;c%B^3hwc*9ozX!04IrXG;gwo5B?+d@>VBxKi`P6F&(|M z1+#>$ttVdT*LhTkxph+?7ODQV&N8zuB25-i9Yf+9w^FEcREje4ps9Sx?zo_b=_reKR^EIq@rp3Lh zGLfpM!$*RS4~9LdTfBWpaP+C6i5(Z0y?x~CC(gB>ITX1cWi{$<^7?K5KgL^V-U%I@ zXl)buoI&i~hpeYfcd=ewF;^>Hp2*G(e{OJrkNr+Mc0+wOD^1E*HKDIotX8tMww%vT zc$&U`>}HS^cexkUPmhVo2X~`* zqtU!9iI1}cXMffkpHt)D^}le=@vidzdWl#0?eoo?9J-A;Z_45`jwfV#9{uc|J?~*8Y%&OGdw4PKq#Z|C|IF|KYet0|Tm3 z@gXG#hrK=&#-E^8vF)SldR~h;_Z7iBMV5DT>^WixvhlgE* z)x^5}g+inpQ@Ij3_oxo}J}2*w^rRf=eaKle{Y0U=-I4L@u6@#nZG2@VX6<`L#I>l; zzD4yeSAf&e5`2J;f&9OlBYKIXy2E__wq(4-gq?co^juqo1PD6 z-mnif-VZ~wII%>006h|1QyE~Q zaCd4KZnQMY*?sDq$f8#NYIehos#;|6G|JyZG_PYCTjjf?UYcHFm5_j-R=uXeP#3%Ikx$dHQX{Lts zJ=zs*I9(a`p60x@KUUxT^kfWIQf|3par(!)bls5gy&D!YN68OJwA`f8a2z|!UTj#s zlu0}m;BD|-dl=5IfITXBK6# z)p`|i2k)&p73aIr6e*jOA#t|Sx%n7R7Kz=~`}wW;;U1dTrS##GWzTh%ssXnt#sJzn zJ=1y;YU@ws6+I6=1&=9kJlMND_pIB(i1(2ULl93x$jFnu_0~6;uL@@;V;EgB(7efL z-uv3=lG=MqBCnqnZPdM{>GZ~qdtIphBK9g*sP_jcc}KnvyR`y2Xp9QQJOcTcwFpLM zPu@679MtkjJiww^MHBr!#}qX0i35(mWCorvie_DYF@Jr@O3E%THg(s-U734QxgT1K zoBQewXPV|LZ5*#V@A^ySWor!4;kWMXY|Q6dy`DCm_eVc3Nk#M8Jr0VcSA9VxnCx?J zG{oP?A1~s(;~P^_tk5~SB5%!zjIXn&)On)G+ufLdM4VZtb-qlo@B0OB8^8AW&kq%i z&ZEZdKAP84@Mx??l-i+?hSmnJ+XtNv@LIm-av%Fu;N9g;>2RdDsD8#?)b99+%E<^n zBFhBvS{^3;RNGEXQ`H*>WErpvDBd(QFZ~((uTe!_BJasp8_Aa@g~PcEbsqN62tTVi zeEo;Xdqo*z^Zonyv{oX%SQ1L?tKc4Zx6zDaWpkzU%tqCe=py?0?*lY%eUff*hgHL! z5K?A&rkMGC^6{AlxSWg<6g#TsVja&n&Mhx! zj@S7}4&`q;ns;IL{KGHZR3h5nM|^b)BD%Zsm~Sx1z3riCB{=r^ES z`c8^LR~J4!S7X_glGDBP0*~l<&1BQo{$VSBGtj(bg~0;s$sfAg=D*`zxp#!Q!Rjur zPXTc%<@B#dmq+%`h!hRI)uHC8zK~6WrTBR?C9Z|~%t%J!ub_rU;u2;r<5B)*qIo%v z2`PVnq}3JgjB{&}rE%Ah;*H9yTx_Ni#QZXaUhE{_jNZF78LC-QWPT5&ewc<&;#esj z=lx;mCN0+Xm*yFD6mJ%q_XSV2wzB8J$6TrBgHw6cQ;TT$%#+G6F;n-+_^O~V}v(dcEi34Sn@{{&GQHNtb(@ z(*&rh&pd1lzQ|_G>3(q6pw;vGcaO#yj-7R$)A2ix?}y^eMf0jRu=ZB@zs+(RIGb%* zdUD@{vvlg>K;hN4#vr)s`_%m2!Qr z*y*2qb?*>iY_~4m#c^5v)I%-S=`MvC(r;!YzMha9`revF!%7>N>Hc0%ZM7`Bv%@Z- zI~<+AkI=j=gN--LKAt7AyO{FLtUrImCBe$4JnZI;_pSmCqhGrHO*{%`TeJA&)w6}t zt5rYLX)&57neU?K@Jf$Z}F%HJ#k#*K{h2y!jOl6z^j+FHy3fF!{Iq zlHrYFYLooJ)umHz5fPR@*3YQ5tyW)T(xMI+AvttRhVO1$(z2jclaW51*@l03b^c3f zo^Zx=NAx;Wgyz+rzj*SojM9jgR#QolYJv4=fLm*M+Q2C5^D~dL@IUEG@o`rt7!pc6 z?isBcombX-p5`4%K&clW{UAms$mD1Z%HLu%FUi61kRJ8@kMijpicf~HRdp2Jz3jNR z?0tT>t7#4SxJ&e5+E+viG5oO=lE(KvoI0WoDvDprx!g$*oS1YpXxCK~ZwZ>WA74JC z%OyAZSw{;Mk6eXvbd(&?w{RymCR3(L`}9+9Hik8`K26+e>q(M!;4^pWi^O^4f1jti z++4FZ@ZxFSDHLxhn%8ch-RU|lUx^YT^Tq>L%G<9k_!{EPoH0`z?)sTK;qjzF&hx2T zm2$YkvHeashnJmsBRK<1+)OC(Eh2_P4O=gvc+1edJ-WjtWA|%6iKHzAII`qD%aYl* zxR}Y95@>YT#e$b^d`5C#hL$a3py2pS+1;DGZH-B?15R^u)}=39ai{rg(Eneg9L-y; z7i+#RrPpNc6Nf>2n$OFxlYaX`#R{3T174J6l%xi-w1mIA{H=FlvHHe^A8li+CQVn< z*m4|bk6t#v^GPBI{l4%Cn%6Frz2t}|hmAqn9zu^!yUaP)==PK>o}gI%gB0h@r;kKp zT~>-*h*PNYf^jcOe7Ln(gZ=-tcOBqSR9$-$dJQF11tPs|Iw7G(K~NA7q)XduCdr0v z?ChoyAoPw@r9_mD5D}0fy(3+^C<0QHA_f5gDZ>ArJ2N|zWHSr#`~KhOe|8^Urks1u zJ@?#m&%JZ!-pRoqhA!!T;6R5~wm&`|uCT)cR6qg3~nQ~3hIe3Z~{ zZdS_eQ8zYu{ZHe%B@}O+{$&2}r`lqZ>#du!{hpQ7tdzb2YB6#STe(Tq#J$&kw zxn}<8heO*vd7L_dOYw&3K$NGA0BDP+*bi%(;*ru-O zwr%g{pEo^r?AkGlws*Ta=-Sz_o!)BI>L4G}x$5F4Ue!xn%dce_&V=*6uM&E2xU zRnsyXf3D#d`rXg^pS>o`-23S)udpfi;%>e6u}|kJQU%DMq>CT!1t>R!7^ zzAGAill=Gfc7MIAkh?=Ex7(5;Yp3=fI_z*@^=_XZKI!#ChBbZqnU#xvnYU#A(5?%{ zR=hr>YwuX==yHp#QOhcIN}MYEG|LUvzl1mj#OKP05eBjZ$H#X0hYv1s!OWS@Ay6e_fy>>-& z{jS&MXTmx>!OwOn<(}@<`AOmL8?1lsy|d8T$&Z6mCtq7JX6MYP7eo4Q%&>>_Pa1Ra zFij)h8Y4PM){t0Ond(J z_|(V4rgURLhS4+#7UvcZO)S}({-My#(#yj)CSH~YUZSbJm z$O3f~a`!0ZZaC=EY(c|6nonrH?DU~|4@bUUq~(EO5es9A9i1Af8~Nt0JAH>X-rILp zaZP%yUUQDOpT56c$3K^a2lw82@yiyz=2WG=dzEslmt8n~($2bx30FssI6l4j8||(h zJsg!$@1to8tBqtl{xPP{bXScF2~ zeM-5rTYCo^i)@l&-+WvdVS&@}G5VjZvF=aZUDtG2EA?;CIZRP0{! z1G?sK-^Oas*zz~_tvNdTaP{_MmGe9Mm2%J5{5d)L?2&zot7eX0cB>SNulqzO1b7U zyVDO&^D9##Bx+C0@Y+qr73Mk~AH4PY&z0lmRG;&r-fIni%`9+!{}WyO_wOIR`Pa9n zL#$&@Wt?@K}lc(RD-u?B2 zkUh!O{=V|rw&-zNF6=K=anroXTcRe7UvTy7R`Y6a&a4xfZ@=Zhr^@zrP$_rV)DwQc z_P_p9XZx3ff}nZYA79<~|3sN<+%UPI%0z8AgohrLNH`~IFVW?qL! zl`BmeSoeoV`uM4x{S^8hQp)u@vSgu8%a5-$3;1TqlS0eeS8P2k`nxetzvT%2BYV`!-_?**=ZUVhn#L&~^*SSdID^M|cEBwhY& zN1Fwk&V4_&Oq<@D0)2bV*veI>uBoI>tVrQEY;Z(1%qyne$cec?L$ z!R^n!ZZLjGK!XZT+F0%H&Zm7AA14=l);?xV$&s@v?><}kWJaak(NAi&S^e!tko!-5G%rs-=_sAkwelWjm^GV`R}s99 z(;E%`@mLH%bb1e=Ooz*V^^eLCYqUg>l9!=4-utA{`G4|9c`Pib(G-R33>^O(dHg5! z^~myA;NNHgYDYGU(de+^yNYEs8lV3{Tm3hd+oO}m0v-$edn`b4h~uLi24l1)qZa#{ zJ!$_Q?L3-zEbz}QKt3MjFj>*nY9tFpNoppMP&5UF1M#3}EKT(+E#TsHcS_l0C^2ql8MBYE)C0$GdqYXAV z!lK5)GmI;Fl+K)NH4*LqT4z`K{cqeO8^jyz2FlB)>Hp7u;U7*f{BKl)Cl8MWJQna+ zz+(ZA1w0n;SioZej|Dsy@L0fO0gnYd7VucWV*!r^JQna+z+(ZA1w0n;SioZej|Dsy z@L0fO0gnYd7VucWV*!r^JQna+z+(ZA1^%xs&{N!VI7-|jShN{$)B79Dyj^EB`Wr3! zfiVUn=ik-FaV`9U1O0eI3TKIF=ojjzGa6#emKf|gW54GwgdbhdzHC~@cZn|Wldfs~ z;uUsBquGfgrO5}>2k5s8pnJ5x?~ZuB8^=OG0pL17KiY>%>9EyJa|56s?IERml=ePA zGHCxN-J?B&j{&+zdpGGG_7!TH0Q4gpkZx3#W&r)Z11McFpanp`;{fT4^|I`5Baw_# z0HwoPWlbbN=}!Z6uO#p$K=;l7bZ@n2=)xy)fnU*Pj*#FMuxBooB_7g^@+JKX;0AqH zfW84h`}JvWJ=u3BunQo2(Y|f64V9a8q;ioCq&MkFdSP!9`?h6i9LoUM?xgVr$^m3+ z+OO>mQS0Q&YDeW#7SsWu0oZoz;fUVUr01r5Tfj}?dL!b}P7w8A{ z2L=E-APUd}(Eta;0I@(EU;yHQu0UnrHQ;ri3Q!fO2KWK~KmZU3ki&eB!;ipu;39Ad zxB`p-Mgpn85P-fBG8~uzj03s>-GLrJC*XacGf*3-1JniT0ri0fz#Bkypau{O1OZop zYrrqSb>Idt1{ehl1$qKqfW|;L&gXbrptv;-o7Hvzhq?uV0;;`jbp z;pnan9!0WGf&4%|0M)|UOf_1(XHK02P7qfGTrU3Z{`33nW`DsUh{Ng=;e4-ujEkhyw-!34jBz17v?5 zumOVrD_{YrJ`#bcz(>FoU@|ZXmKEi6=pPisJcl%R<9tBBI{n_b4oV}UU=NO$ zuotSz_ai@?ed)kBkrId&qSKQCn#`>QpF3{(v;ZZ{zp;N%s74=W8EA;s*6p#5-B4r(`3MruMYS3a~Klu-W=e}o8W z$787)&08fFy1x;WK>y$nh~tttJ^AmDUcIUe_;beVpujeyHGNqj3NAn8+<;+K%r8!Z z5(ENx;w{Ng0)0oZgi(IJkEeqYBJj{(EF-(|e(0btq>8Pl$gA1UkQXQGf2Lsf$yLgFy*`-Gb3Bz*`a0 zj?{ha^6%{?ukiKi*VsRVj7?t*hG(UX?sH(k@b`+|0tFh=&uZcAdY;$x(H%Q<`@O=$ zK?x^!3uZln(sz8lS~L%Z^8{*Ay;(nzHm}^qTa`y|9S98qQ9saN1}K!z*^&|FGUeL8 z%Sy^@AbsB=rpM&W%Dpc12}rlSG~ZkD{;Lx;j2k=CQ|dWFYUg=CX@@!2@P)dPPsZ1GT*BUjX-9P*xN_`}{?#GvuS>*PwV66r{aB)VTGhHD70d0!tI6xJcRF#jwZw`KhU(gn|NF zTXpt0?LaR1B){YNnJ&$W$ob3wj}P*REO%*klO2VJfD$Z7ON=w<<4~U6J- zNP#p1k2Va-@`gR|mw#`4FZ09{66&Yu%^p=+em?05%ZKTf)1nOvv$0EB8yv|K|JXak zn*RNfQ{aKWK?%(>P)N64J2DHTU;MqUtU+fmlmw+m=hGef-aB}icm%C4fI=QJf5r6Q z%9lGi8kBHQP(QaoAzLp`er^f*{IxgpGk+svC!q0=Km2XAQR zCxhd5_nf)J+iL{wRKT4taKyi~D%CI7*jc3dCh$-#z&ZzTn{N z<#6XLj#Scar*79wv6Sk?bYr!B9~9C$sOa6_>bLRD@9lL7cc^tVf#ZF2g-1Wg5b2?Ug_uCJo=1$RO~Tw;X!!{!8D}7?daa!E;QTO znrY3Xl>vopkzom1XUx>k2L%D1@_7mhl_zRY&E}ECe8NE?uY$C8xJ??I=(*zfyMKQ6 zi?`QM3h`(qE=n3El*dI$bJtp|1s5e1qrZ!iiaah#8X?5%5vWkA(_Z_-y4-5__%_NT zxTxkd9F*$z^7Ess>V}q>4GP70l=LPjl|V_pyZ+qymRp7RhS(d*{(Q)5pgbf^hSCyMca{^W=&o>0y5(t<@xSK9YU#pB^;!6U|lOi-v6 zj(t_V>71@>c7YNqL}j}z6j2*CkGXtq&iYXluc`e&>!;u;1D?h8FHUs4Tdy7CVeQB2 zFryQW=IXVG8sE5Nfxe)ioK)Lw(almmE&6$HJ$WB)X zGsj)b?==+T25MPd!9(TQ;#EAIzj#KzSXM^)GJUD_Z$bEtxVAlxm=K zo|~B&`T2?GGGzrQ7$>C_w4aPN9=q=?Qx1qcU)RdtH}J;jvNGkmND2RAaHo=`#-5NV z1<~-TL)yfT{!ZUmqx3DAQWumOp#0Tu&d|{GLoZ}Xdr(M&U*D`%_(7}Idu56N6hBZJ z*ZDF$`04RBGG#m{H9?s^{PkW#6CTu+DPM}xx?bF|VCSCsOhb|H*tKoe*;5|CuO*M42nrfx+QjIYYqtJ)P@u3@GEb!J zXgKf3rrj!1`=NLcg6APo+WawFS}p&(5sKcEXi* zrvf`prs#r^CE38nId~es`P6*7Ys*;;`!EW#LC#vBPKKm4dqe2a#}=wLzr&)n%NP$E zTaE*T#uQDfz0)scdb{?F!p7uX(d4P5tA5&Y`whM!`GMHeTFji?VB$1AnylTvJ<2SKR--0w6 zp&TxJvv#|VhmOekbQCEGJ%X!DPVIOI6tNb1gF;bx$;b0=7w{YKCgWidHWrk!pjf+o z*YkQ0+b~(0%W)!Wb0&kypx4Hjzv|n%MzK%QZ^_a!YY9EZ#fv*WT^YPBBEJ_J0`oVT zRzgrpJ5cETPukot?UpINpwQ^HON-+TUawMmnM|oK@>G2CB&f@wFixhl00rSHZT@S4 zn@`=;_m(LifI{U7d~0t1jRk!W{Ur@z#C%FrXnk}3s)TQ4N~)O8ZcS#}+Bdx;Wy(}g zs0TD`YPLMIpy^kcvQXqXKQg9G(8oXSk|`TNAq^IOF*H82aIKRv)_SU`GNv1p$^Qm8^Ziv^!s|#dG(K>=O`Rw{-+VZj$+CGvgvqYW)57VlpF77r%rhEYkX;8Ryiw56# z%{(Ggwu;ii-aE2k!Sr=0GR4HBf#bJ$-QF|nzyIhRA%-C6Yjoyhlg_S>`^)ioRJY%z zugBPu`Ye!gMoUmwyv`Ywhl*{xcYqKRTl=hDW}kd~0;GxKot%+xsMzM>M4r@wedjfw zw8YFbVC~1o>7wboDAlWdQmIPyE)yAr#kbtYNG>BebijN&;$msoEqc<1?uMV{XUZ)& zrv|KFKzf(bSR8Ezi4-aP3*?*Ib!qw9kOtdOUy^+mrxf~n3=_jKvj`sYx3?nuR2Z}( zr6YLAqUf3Qn3160nZM^<*jr~5Q4sqn{O3OR#PNEaQ5VfUGw%7j!-q-znKV{&E_-J- zf|S1dGv$YgK93h=c!NiL{<#3%C|a)TGJVwzi_O5Kv4~`~SxiK=vwdoYiI^T z8f}PQDk}HxFWpN_uXz=e#^`@wgMmiQVMBR(G#@dk@22(?Va2FC00pHOzwoz|2KGMp z$ATgbd@+k=)!BH?+b8o(_uUt1UQB%cvFS0ez^@^<@1wf=T0dtLR?=uL#zB-(-Rrd3 zbG_72Q0Vyw7R^55V)-|S=soAUg&lN-pN*QgjbLIr}-)s6zA2#?5`CCvJ z23Cz|ZZIawlBB7;yZ@D$b%SXB4G!Yp7=DoJeBx8&MZIRUZ$_~V?=c&|JxJ^PC~YZF zw)VJpG+G}U%QO)BA6=|Lk1;~*_4@C0=vUc}aUxAhk-z1tLGIScy`<8YnSOZmQS|1| zuib?P7^jkXa_7mtJdPL(RgcE|mn#o@Z5&Lsjo}X2T5qu1sp`fKS`~SC^XQ(U-SAYV zHrtw;hcpLYY1r*i^M##OS+s>&KFoWiuPpW1H+y)k7RPChnCylYoEJZ^>!>AQ=BLS= zn7(hFm;q^F!pIx7ooEKJ<-YF+Mds5k1VtPPN#BFo@ib<0!uHQrGTqonH5scC47^=) z?~~PME2q9eV@q+wm2~J7M5SkjZ}IAOt1ioj`A#ZqK%@2Fe*a|Vz(+=k z2a*OkXY;59L)t58;Fc#Zb?Ra^oF19b7XGvT=3)#hjz?XLVa1ZV7{iLXz0w#~jLNSx zhNV6j_2Xg;OV1^sxERBVT9e(f&2>7xD#sXBq~xW|MRUBJT0`2yo8Rhd)~zr48`}1r zrb2&uYQmk&5@nvy*q{0Z)M@T^dj=lz-hRcmCmp?Tqd9n}or0%OGlBBX?RAU#+*%e1 z3TcfNT)4T=&lRyW+kdO`+$|VWpvSyV@uq>^ zvDz4m4Wmt%FwSnb@=XE)Y+Nj&nJwAhY&FI6{uWzofQ!@tT32_q%t3Ri1GQ0{UFRRjTO2k$r#0!UcC2gD4m22zk!a^O3vY?B2k5K@Eth1+ zY>C}si{uma0r>PHr>3SPgmjh8Q9YO=U<$+gMUNB@%noF0<+Vx%Vlt{V;mx5|cwI=+ zBjas`=vXe1Ga3V8IWrZ?ingh@m`plzG-op!%p9-9DQseNn6R*q*BZ?5!5GX2Y4vfO zexRz?xJu6IB&QTD4da4BNGp}Xh_Ck`Lp;+`YzlM3Wl8~qDMicP5mH(_xoK%^jyFq| zsCZ6q*W%?HruoEocUlY-RFqJpqQVI8`-lhCMx7ioXwlq{9t8van)!vgm!yz_x)EBF z3YS8Aa4N2lKXpp#Cd^V`@MpP(IcaFDE+L0kKuV5#%!TFl%pu0XY5ftYR7yrOrVvI4 zEndpeOJ%4MuhS3Yt(-&TAFBO4UmuHqIBk1Gsf!QL)JqH*D7XZ>5&0DqUF&OzPekn z*Sz3hN5yTdc^)TCbMYDX^B!=E>mhRvld5Vpge_Y$+iw+Z|Tb!Z-v|J7zr>jou8Cb80fP zBqWhpZm^c_u7J+i1a{{+>YJ7&!8DNuUZ>OZG@Nl5BCP{s<9T=mW@<2FqZSCWCxvt; z8}*bFgw!pcC1^r)ik7!08#y&0vR;rPUudbP zX8yv&xQh#NB1n-hFlrU0s1$=Tm255QAYCG2)5|hcC1kiKBwO~Y+G&Z!lTjnSoa z5SHayZur7&v(BUz1m#3dLBv5pR(SkSF|*SL!RkDRG2FLC+3&zAU$7YZQif!X0bYgc zIUQIQ=f?;V)yVV+aR8+2yoaVpZdED51a)*A>+)X%HOC50Q20NLx&D~QiLDjMYO zR5UN>Zs6*Cu0hVu^OqX(=JKEfIf}<7lpkwRsr<_1Jcp+Xn@yLjyh#s;>=}|Cx0D3o z=@QRjtl{fbOrgRBwX(^9dsC!RuNtMqEDAzimTOkf;2b5Do|qjr!%OO|;6NUVD>h#@L12R6!a*Knk@Sm2RP5q8}_ zsb(oHn6q3%QyRL$v}{&~o-Bm*&??R&B!DF01R`i55NmS6>={jxa$KaCPE3VW=7|O~ zTA?W{G#d$X8#ap}+9eGqrvGv1W$~)#Tuf*~p4~wVW=>kd5q= zt%YQw$HnBDf~31lBIs|>p%A&MZ!_@vgs@P`#$^UpOCo2p>cc`YYnEFYJ|rSA$puw3 zdr}Ug3yR@>?s^e1xBM6*3a#QNF3TeE$?hYVS-P+Aro4!Tx~?SScAf-hHIghj>-{{5 zmMcaUE8okLNWqd$PmtwiBm4v5*<3Mu7TL)EAR|Sa$exLUl&1``HAz`~HbS0+%Rxg9 z333jusQ)k(TH1TYIH zZ2ZH9vMMoO8ky3&AW~iNYKYAMxEoo!c)bKMF7C>+5=Mhjcn_tbmNN~6InQ5e=q$ha zB0Ze1!Cq=GC~S@cpK!wRb2osb8U3ha@CYYbHlfL^7K3L>wy>8yBWWFG^?5)=Do9gY z!3EquhC4F@uk#!QzydEehNPKpEcSq3`Qwq;9;vua)ENh|_Y#q^uQpL4Cn94xIoP8N zt8TkRn67dwDQiMV%X&keL&Id1*stc`*tejAvUE zT$u(dBF$JZs-g`wm_a0ljt~+=F96idC$6OjlX%7g40|i6l93Cw1nor~S@~JWwmHmN zv`4&p<~Hdl6%x z_p4?u5yLz+;|NoIrFo->`@m04VZ2tM)brDC?U@dRwr zsa!)eMXO6QZ1Q=IdTB46PiO)_OmX0%w$u{jc&3|2lYlrv^aoYfM7Oz~Me zTS_*?knNsQYwVq(;wf)gteiP#k)^xJl)S@i;j{269;uv}%FpWAZb6iaTjWd(4-sU6 zQQ;#mFN4hP6v8I@WKVl}DeO_wDMom(d^I^vyFrTc{1r>2s1Ha{T;)-YR4PeQUdkmq z6I7BfUQr{#ol(aS;yllj4Cee!DZ*^qtJPGNrsb3*O>5;v7HhgrN$e_5Ws@R$^_Hpp_K`Yl0Pc0@}v#qI7@}|SCpX$^pK*s%A*{qRFWii{fe^W zsb{DpU*t`Sq>!>dd__spRF}|V6<2wbBdH`w%8QK@yfBjD>Q!};h8-kDI)6DC?Bh#R zL-Y|Qj5_ISP3n(-CbnLTXX7V#FLT8$5s)vQVPq*TjZx{zq)lJMChq?NpSTDY!$l0( zbT+K@#U2gp;Wy#4qbXQTgDVx8($-1Hlul)tDtuWp0C(0KY^3aNoS#)?$jEvl%Se<8 zh8Y2lEuB+)b1WJ`P7Ap??m@J;Do3S=(vDXMkWNu7T6L`M2w0g27W|JPKV}WwO#ozF zrBB*sS+ATP@`dB3H8jtP41_Hr7g2ylTKkX_lxjzXu&K117wbW_ zY}tYpJ1S@w0z#|BfwikDuADt7gl50RYRz4wapp||Bt1ws_r6Bkc8QCQjKf{oX=5xH z=z#R@rwy@<70K~GQj42{$Q#&RSryYTIvClJmgTu${6mog+lj#>o*@{JhU(!=*q#g~ z;Y2I!O?Jb?_P>IM9c4pHJDS|>j4)u{1l+N71h?2VvZeVk9M(B&%{sdwLCtfpnHB=@ zKZ3AmnHU}up2Es*6}u`@L4e{)?v7Nrr3}C-ow7#mZtx)Pl?9u4_EK|`lm+eA6?3Cg zrlngKb+N$R+0Bsi=}pnWn`N>SaV zjS#%j=}Y;NJW~ly`GT!}b~pLxoT>z?^Zcdkm2=vXhO_RhEX`FSJL&YL%Efu@`a}g-=|I7da#PO>-@!n7 z{QpwXfbF$rs=@17t8_P{aW(jZ0BIb^YM$8xA09DkvEJOIOOB!y2(&W*1CRt72cZAQ zhF`Un@QPHBuDE)sF^?iUa4W7@AaNHWvuF= payload.nbf; + } + + if (opts.iat !== undefined) { + result.iat = payload.iat === opts.iat; + } + + if (opts.iss !== undefined) { + result.iss = payload.iss === opts.iss; + } + + if (opts.jti !== undefined) { + result.jti = payload.jti !== opts.jti; + } + + if (opts.sub !== undefined) { + result.sub = payload.sub === opts.sub; + } + + if (opts.aud !== undefined) { + result.aud = payload.aud === opts.aud; + } + + return result; +} + +const jwt = { encode, decode, verify }; +export { decode, encode, verify }; +export default jwt; diff --git a/packages/jwt/tsconfig.json b/packages/jwt/tsconfig.json new file mode 100644 index 0000000..7fc5833 --- /dev/null +++ b/packages/jwt/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "esnext", + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist" + } +} diff --git a/packages/jwt/tsup.config.ts b/packages/jwt/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/jwt/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +}); diff --git a/packages/keepalive-ws/.npmignore b/packages/keepalive-ws/.npmignore new file mode 100644 index 0000000..bf40d27 --- /dev/null +++ b/packages/keepalive-ws/.npmignore @@ -0,0 +1,2 @@ +node_modules +src \ No newline at end of file diff --git a/packages/keepalive-ws/README.md b/packages/keepalive-ws/README.md new file mode 100644 index 0000000..f00f130 --- /dev/null +++ b/packages/keepalive-ws/README.md @@ -0,0 +1,98 @@ +For a TCP-based, node-only solution with a similar API, see [duplex](https://github.com/node-prism/duplex). + +# keepalive-ws + +A command server and client for simplified WebSocket communication, with builtin ping and latency messaging. + +Built for [grove](https://github.com/node-prism/grove), but works anywhere. + +### Server + +For node. + +```typescript +import { KeepAliveServer, WSContext } from "@prsm/keepalive-ws/server"; + +const ws = new KeepAliveServer({ + // Where to mount this server and listen to messages. + path: "/", + // How often to send ping messages to connected clients. + pingInterval: 30_000, + // Calculate round-trip time and send latency updates + // to clients every 5s. + latencyInterval: 5_000, +}); + +ws.registerCommand( + "authenticate", + async (c: WSContext) => { + // use c.payload to authenticate c.connection + return { ok: true, token: "..." }; + }, +); + +ws.registerCommand( + "throws", + async (c: WSContext) => { + throw new Error("oops"); + }, +); +``` + +Extended API: + +- Rooms + + It can be useful to collect connections into rooms. + + - `addToRoom(roomName: string, connection: Connection): void` + - `removeFromRoom(roomName: string, connection: Connection): void` + - `getRoom(roomName: string): Connection[]` + - `clearRoom(roomName: string): void` +- Command middleware +- Broadcasting to: + - all + - `broadcast(command: string, payload: any, connections?: Connection[]): void` + - all connections that share the same IP + - `broadcastRemoteAddress(c: Connection, command: string, payload: any): void` + - rooms + - `broadcastRoom(roomName: string, command: string, payload: any): void` + +### Client + +For the browser. + +```typescript +import { KeepAliveClient } from "@prsm/keepalive-ws/client"; + +const opts = { + // After 30s (+ maxLatency) of no ping, assume we've disconnected and attempt a + // reconnection if shouldReconnect is true. + // This number should be coordinated with the pingInterval from KeepAliveServer. + pingTimeout: 30_000, + // Try to reconnect whenever we are disconnected. + shouldReconnect: true, + // This number, added to pingTimeout, is the maximum amount of time + // that can pass before the connection is considered closed. + // In this case, 32s. + maxLatency: 2_000, + // How often to try and connect during reconnection phase. + reconnectInterval: 2_000, + // How many times to try and reconnect before giving up. + maxReconnectAttempts: Infinity, +}; + +const ws = new KeepAliveClient("ws://localhost:8080", opts); + +const { ok, token } = await ws.command("authenticate", { + username: "user", + password: "pass", +}); + +const result = await ws.command("throws", {}); +// result is: { error: "oops" } + +ws.on("latency", (e: CustomEvent<{ latency: number }>) => { + // e.detail.latency is round-trip time in ms +}); +``` diff --git a/packages/keepalive-ws/bump.config.ts b/packages/keepalive-ws/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/keepalive-ws/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/keepalive-ws/bun.lockb b/packages/keepalive-ws/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..56b08aac8f3bb97459d42a7ae9ae9031d42765f0 GIT binary patch literal 77925 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 + +const defaultOptions = (opts: KeepAliveClientOptions = {}) => { + opts.pingTimeout = opts.pingTimeout ?? 30_000; + opts.maxLatency = opts.maxLatency ?? 2_000; + opts.shouldReconnect = opts.shouldReconnect ?? true; + opts.reconnectInterval = opts.reconnectInterval ?? 2_000; + opts.maxReconnectAttempts = opts.maxReconnectAttempts ?? Infinity; + return opts; +}; + +export class KeepAliveClient extends EventTarget { + connection: Connection; + url: string; + socket: WebSocket; + pingTimeout: ReturnType; + options: KeepAliveClientOptions; + isReconnecting = false; + + constructor(url: string, opts: KeepAliveClientOptions = {}) { + super(); + this.url = url; + this.socket = new WebSocket(url); + this.connection = new Connection(this.socket); + this.options = defaultOptions(opts); + this.applyListeners(); + } + + get on() { + return this.connection.addEventListener.bind(this.connection); + } + + applyListeners() { + this.connection.addEventListener("connection", () => { + this.heartbeat(); + }); + + this.connection.addEventListener("close", () => { + this.reconnect(); + }); + + this.connection.addEventListener("ping", () => { + this.heartbeat(); + }); + + this.connection.addEventListener( + "message", + (ev: CustomEventInit) => { + this.dispatchEvent(new CustomEvent("message", ev)); + }, + ); + } + + heartbeat() { + clearTimeout(this.pingTimeout); + + this.pingTimeout = setTimeout(() => { + if (this.options.shouldReconnect) { + this.reconnect(); + } + }, this.options.pingTimeout + this.options.maxLatency); + } + + async reconnect() { + if (this.isReconnecting) { + return; + } + + this.isReconnecting = true; + + let attempt = 1; + + if (this.socket) { + try { + this.socket.close(); + } catch (e) {} + } + + const connect = () => { + this.socket = new WebSocket(this.url); + this.socket.onerror = () => { + attempt++; + + if (attempt <= this.options.maxReconnectAttempts) { + setTimeout(connect, this.options.reconnectInterval); + } else { + this.isReconnecting = false; + + this.connection.dispatchEvent(new Event("reconnectfailed")); + this.connection.dispatchEvent(new Event("reconnectionfailed")); + } + }; + + this.socket.onopen = () => { + this.isReconnecting = false; + this.connection.socket = this.socket; + + this.connection.applyListeners(true); + + this.connection.dispatchEvent(new Event("connection")); + this.connection.dispatchEvent(new Event("connected")); + this.connection.dispatchEvent(new Event("connect")); + + this.connection.dispatchEvent(new Event("reconnection")); + this.connection.dispatchEvent(new Event("reconnected")); + this.connection.dispatchEvent(new Event("reconnect")); + }; + }; + + connect(); + } + + async command( + command: string, + payload?: any, + expiresIn?: number, + callback?: Function, + ) { + return this.connection.command(command, payload, expiresIn, callback); + } +} diff --git a/packages/keepalive-ws/src/client/connection.ts b/packages/keepalive-ws/src/client/connection.ts new file mode 100644 index 0000000..b04cce8 --- /dev/null +++ b/packages/keepalive-ws/src/client/connection.ts @@ -0,0 +1,198 @@ +import { IdManager } from "./ids"; +import { Queue, QueueItem } from "./queue"; + +type Command = { + id?: number; + command: string; + payload?: any; +}; + +type LatencyPayload = { + /** Round trip time in milliseconds. */ + latency: number; +}; + +export declare interface Connection extends EventTarget { + addEventListener(type: "message", listener: (ev: CustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + + /** Emits when a connection is made. */ + addEventListener(type: "connection", listener: () => any, options?: boolean | AddEventListenerOptions): void; + /** Emits when a connection is made. */ + addEventListener(type: "connected", listener: () => any, options?: boolean | AddEventListenerOptions): void; + /** Emits when a connection is made. */ + addEventListener(type: "connect", listener: () => any, options?: boolean | AddEventListenerOptions): void; + + /** Emits when a connection is closed. */ + addEventListener(type: "close", listener: () => any, options?: boolean | AddEventListenerOptions): void; + /** Emits when a connection is closed. */ + addEventListener(type: "closed", listener: () => any, options?: boolean | AddEventListenerOptions): void; + /** Emits when a connection is closed. */ + addEventListener(type: "disconnect", listener: () => any, options?: boolean | AddEventListenerOptions): void; + /** Emits when a connection is closed. */ + addEventListener(type: "disconnected", listener: () => any, options?: boolean | AddEventListenerOptions): void; + + /** Emits when a reconnect event is successful. */ + addEventListener(type: "reconnect", listener: () => any, options?: boolean | AddEventListenerOptions): void; + + /** Emits when a reconnect fails after @see KeepAliveClientOptions.maxReconnectAttempts attempts. */ + addEventListener(type: "reconnectfailed", listener: () => any, options?: boolean | AddEventListenerOptions): void; + + /** Emits when a ping message is received from @see KeepAliveServer from `@prsm/keepalive-ws/server`. */ + addEventListener(type: "ping", listener: (ev: CustomEventInit<{}>) => any, options?: boolean | AddEventListenerOptions): void; + + /** Emits when a latency event is received from @see KeepAliveServer from `@prsm/keepalive-ws/server`. */ + addEventListener(type: "latency", listener: (ev: CustomEventInit) => any, options?: boolean | AddEventListenerOptions): void; + + addEventListener(type: string, listener: (ev: CustomEvent) => any, options?: boolean | AddEventListenerOptions): void; +} + +export class Connection extends EventTarget { + socket: WebSocket; + ids = new IdManager(); + queue = new Queue(); + callbacks: { [id: number]: (error: Error | null, result?: any) => void } = {}; + + constructor(socket: WebSocket) { + super(); + this.socket = socket; + this.applyListeners(); + } + + /** + * Adds an event listener to the target. + * @param event The name of the event to listen for. + * @param listener The function to call when the event is fired. + * @param options An options object that specifies characteristics about the event listener. + */ + on(event: string, listener: (ev: CustomEvent) => any, options?: boolean | AddEventListenerOptions) { + this.addEventListener(event, listener, options); + } + + /** + * Removes the event listener previously registered with addEventListener. + * @param event A string that specifies the name of the event for which to remove an event listener. + * @param listener The event listener to be removed. + * @param options An options object that specifies characteristics about the event listener. + */ + off(event: string, listener: (ev: CustomEvent) => any, options?: boolean | AddEventListenerOptions) { + this.removeEventListener(event, listener, options); + } + + sendToken(cmd: Command, expiresIn: number) { + try { + this.socket.send(JSON.stringify(cmd)); + } catch (e) { + this.queue.add(cmd, expiresIn); + } + } + + applyListeners(reconnection = false) { + const drainQueue = () => { + while (!this.queue.isEmpty) { + const item = this.queue.pop() as QueueItem; + this.sendToken(item.value, item.expiresIn); + } + }; + + if (reconnection) drainQueue(); + + // @ts-ignore + this.socket.onopen = (socket: WebSocket, ev: Event): any => { + drainQueue(); + this.dispatchEvent(new Event("connection")); + this.dispatchEvent(new Event("connected")); + this.dispatchEvent(new Event("connect")); + }; + + this.socket.onclose = (event: CloseEvent) => { + this.dispatchEvent(new Event("close")); + this.dispatchEvent(new Event("closed")); + this.dispatchEvent(new Event("disconnected")); + this.dispatchEvent(new Event("disconnect")); + }; + + this.socket.onmessage = async (event: MessageEvent) => { + try { + const data = JSON.parse(event.data); + + this.dispatchEvent(new CustomEvent("message", { detail: data })); + + if (data.command === "latency:request") { + this.dispatchEvent( + new CustomEvent( + "latency:request", + { detail: { latency: data.payload.latency ?? undefined }} + ) + ); + this.command("latency:response", { latency: data.payload.latency ?? undefined }, null); + } else if (data.command === "latency") { + this.dispatchEvent( + new CustomEvent( + "latency", + { detail: { latency: data.payload ?? undefined }} + ) + ); + } else if (data.command === "ping") { + this.dispatchEvent(new CustomEvent("ping", {})); + this.command("pong", {}, null); + } else { + this.dispatchEvent(new CustomEvent(data.command, { detail: data.payload })); + } + + if (this.callbacks[data.id]) { + this.callbacks[data.id](null, data.payload); + } + } catch (e) { + this.dispatchEvent(new Event("error")); + } + }; + } + + async command(command: string, payload: any, expiresIn: number = 30_000, callback: Function | null = null) { + const id = this.ids.reserve(); + const cmd = { id, command, payload: payload ?? {} }; + + this.sendToken(cmd, expiresIn); + + if (expiresIn === null) { + this.ids.release(id); + delete this.callbacks[id]; + return null; + } + + const response = this.createResponsePromise(id); + const timeout = this.createTimeoutPromise(id, expiresIn); + + if (typeof callback === "function") { + const ret = await Promise.race([response, timeout]); + callback(ret); + return ret; + } else { + return Promise.race([response, timeout]); + } + } + + createTimeoutPromise(id: number, expiresIn: number) { + return new Promise((_, reject) => { + setTimeout(() => { + this.ids.release(id); + delete this.callbacks[id]; + reject(new Error(`Command ${id} timed out after ${expiresIn}ms.`)); + }, expiresIn); + }); + } + + createResponsePromise(id: number) { + return new Promise((resolve, reject) => { + this.callbacks[id] = (error: Error | null, result?: any) => { + this.ids.release(id); + delete this.callbacks[id]; + if (error) { + reject(error); + } else { + resolve(result); + } + }; + }); + } +} diff --git a/packages/keepalive-ws/src/client/ids.ts b/packages/keepalive-ws/src/client/ids.ts new file mode 100644 index 0000000..1d0d252 --- /dev/null +++ b/packages/keepalive-ws/src/client/ids.ts @@ -0,0 +1,44 @@ +export class IdManager { + ids: Array = []; + index: number = 0; + maxIndex: number; + + constructor(maxIndex: number = 2 ** 16 - 1) { + this.maxIndex = maxIndex; + } + + release(id: number) { + if (id < 0 || id > this.maxIndex) { + throw new TypeError( + `ID must be between 0 and ${this.maxIndex}. Got ${id}.`, + ); + } + this.ids[id] = false; + } + + reserve(): number { + const startIndex = this.index; + + while (true) { + const i = this.index; + + if (!this.ids[i]) { + this.ids[i] = true; + + return i; + } + + if (this.index >= this.maxIndex) { + this.index = 0; + } else { + this.index++; + } + + if (this.index === startIndex) { + throw new Error( + `All IDs are reserved. Make sure to release IDs when they are no longer used.`, + ); + } + } + } +} diff --git a/packages/keepalive-ws/src/client/index.ts b/packages/keepalive-ws/src/client/index.ts new file mode 100644 index 0000000..e5a5473 --- /dev/null +++ b/packages/keepalive-ws/src/client/index.ts @@ -0,0 +1,2 @@ +export { KeepAliveClient } from "./client"; +export { Connection } from "./connection"; diff --git a/packages/keepalive-ws/src/client/queue.ts b/packages/keepalive-ws/src/client/queue.ts new file mode 100644 index 0000000..948754e --- /dev/null +++ b/packages/keepalive-ws/src/client/queue.ts @@ -0,0 +1,50 @@ +export class QueueItem { + value: any; + expireTime: number; + + constructor(value: any, expiresIn: number) { + this.value = value; + this.expireTime = Date.now() + expiresIn; + } + + get expiresIn() { + return this.expireTime - Date.now(); + } + + get isExpired() { + return Date.now() > this.expireTime; + } +} + +export class Queue { + items: any[] = []; + + add(item: any, expiresIn: number) { + this.items.push(new QueueItem(item, expiresIn)); + } + + get isEmpty() { + let i = this.items.length; + + while (i--) { + if (this.items[i].isExpired) { + this.items.splice(i, 1); + } else { + return false; + } + } + + return true; + } + + pop(): QueueItem | null { + while (this.items.length) { + const item = this.items.shift() as QueueItem; + if (!item.isExpired) { + return item; + } + } + + return null; + } +} diff --git a/packages/keepalive-ws/src/index.ts b/packages/keepalive-ws/src/index.ts new file mode 100644 index 0000000..80a56be --- /dev/null +++ b/packages/keepalive-ws/src/index.ts @@ -0,0 +1,2 @@ +export { KeepAliveClient } from "./client"; +export { KeepAliveServer } from "./server"; diff --git a/packages/keepalive-ws/src/server/command.ts b/packages/keepalive-ws/src/server/command.ts new file mode 100644 index 0000000..6381a42 --- /dev/null +++ b/packages/keepalive-ws/src/server/command.ts @@ -0,0 +1,19 @@ +export interface Command { + id?: number; + command: string; + payload: any; +} + +export const bufferToCommand = (buffer: Buffer): Command => { + const decoded = new TextDecoder("utf-8").decode(buffer); + if (!decoded) { + return { id: 0, command: "", payload: {} }; + } + + try { + const parsed = JSON.parse(decoded) as Command; + return { id: parsed.id, command: parsed.command, payload: parsed.payload }; + } catch (e) { + return { id: 0, command: "", payload: {} }; + } +}; diff --git a/packages/keepalive-ws/src/server/connection.ts b/packages/keepalive-ws/src/server/connection.ts new file mode 100644 index 0000000..db0a4ef --- /dev/null +++ b/packages/keepalive-ws/src/server/connection.ts @@ -0,0 +1,88 @@ +import EventEmitter from "node:events"; +import { IncomingMessage } from "node:http"; +import { WebSocket } from "ws"; +import { KeepAliveServerOptions } from "."; +import { bufferToCommand, Command } from "./command"; +import { Latency } from "./latency"; +import { Ping } from "./ping"; + +export class Connection extends EventEmitter { + id: string; + socket: WebSocket; + alive = true; + latency: Latency; + ping: Ping; + remoteAddress: string; + connectionOptions: KeepAliveServerOptions; + + constructor( + socket: WebSocket, + req: IncomingMessage, + options: KeepAliveServerOptions, + ) { + super(); + this.socket = socket; + this.id = req.headers["sec-websocket-key"]!; + this.remoteAddress = req.socket.remoteAddress!; + this.connectionOptions = options; + + this.applyListeners(); + this.startIntervals(); + } + + startIntervals() { + this.latency = new Latency(); + this.ping = new Ping(); + + this.latency.interval = setInterval(() => { + if (!this.alive) { + return; + } + + if (typeof this.latency.ms === "number") { + this.send({ command: "latency", payload: this.latency.ms }); + } + + this.latency.onRequest(); + this.send({ command: "latency:request", payload: {} }); + }, this.connectionOptions.latencyInterval); + + this.ping.interval = setInterval(() => { + if (!this.alive) { + this.emit("close"); + } + + this.alive = false; + this.send({ command: "ping", payload: {} }); + }, this.connectionOptions.pingInterval); + } + + stopIntervals() { + clearInterval(this.latency.interval); + clearInterval(this.ping.interval); + } + + applyListeners() { + this.socket.on("close", () => { + this.emit("close"); + }); + + this.socket.on("message", (buffer: Buffer) => { + const command = bufferToCommand(buffer); + + if (command.command === "latency:response") { + this.latency.onResponse(); + return; + } else if (command.command === "pong") { + this.alive = true; + return; + } + + this.emit("message", buffer); + }); + } + + send(cmd: Command) { + this.socket.send(JSON.stringify(cmd)); + } +} diff --git a/packages/keepalive-ws/src/server/index.ts b/packages/keepalive-ws/src/server/index.ts new file mode 100644 index 0000000..66c888d --- /dev/null +++ b/packages/keepalive-ws/src/server/index.ts @@ -0,0 +1,294 @@ +import { IncomingMessage } from "node:http"; +import { ServerOptions, WebSocket, WebSocketServer } from "ws"; +import { bufferToCommand } from "./command"; +import { Connection } from "./connection"; + +export declare interface KeepAliveServer extends WebSocketServer { + on(event: "connection", handler: (socket: WebSocket, req: IncomingMessage) => void): this; + on(event: "connected", handler: (c: Connection) => void): this; + on(event: "close", handler: (c: Connection) => void): this; + on(event: "error", cb: (this: WebSocketServer, error: Error) => void): this; + on(event: "headers", cb: (this: WebSocketServer, headers: string[], request: IncomingMessage) => void): this; + on(event: string | symbol, listener: (this: WebSocketServer, ...args: any[]) => void): this; + + emit(event: "connection", socket: WebSocket, req: IncomingMessage): boolean; + emit(event: "connected", connection: Connection): boolean; + emit(event: "close", connection: Connection): boolean; + emit(event: "error", connection: Connection): boolean; + + once(event: "connection", cb: (this: WebSocketServer, socket: WebSocket, request: IncomingMessage) => void): this; + once(event: "error", cb: (this: WebSocketServer, error: Error) => void): this; + once(event: "headers", cb: (this: WebSocketServer, headers: string[], request: IncomingMessage) => void): this; + once(event: "close" | "listening", cb: (this: WebSocketServer) => void): this; + once(event: string | symbol, listener: (this: WebSocketServer, ...args: any[]) => void): this; + + off(event: "connection", cb: (this: WebSocketServer, socket: WebSocket, request: IncomingMessage) => void): this; + off(event: "error", cb: (this: WebSocketServer, error: Error) => void): this; + off(event: "headers", cb: (this: WebSocketServer, headers: string[], request: IncomingMessage) => void): this; + off(event: "close" | "listening", cb: (this: WebSocketServer) => void): this; + off(event: string | symbol, listener: (this: WebSocketServer, ...args: any[]) => void): this; + + addListener(event: "connection", cb: (client: WebSocket, request: IncomingMessage) => void): this; + addListener(event: "error", cb: (err: Error) => void): this; + addListener(event: "headers", cb: (headers: string[], request: IncomingMessage) => void): this; + addListener(event: "close" | "listening", cb: () => void): this; + addListener(event: string | symbol, listener: (...args: any[]) => void): this; + + removeListener(event: "connection", cb: (client: WebSocket) => void): this; + removeListener(event: "error", cb: (err: Error) => void): this; + removeListener(event: "headers", cb: (headers: string[], request: IncomingMessage) => void): this; + removeListener(event: "close" | "listening", cb: () => void): this; + removeListener(event: string | symbol, listener: (...args: any[]) => void): this; +} +export class WSContext { + wss: KeepAliveServer; + connection: Connection; + payload: any; + + constructor(wss: KeepAliveServer, connection: Connection, payload: any) { + this.wss = wss; + this.connection = connection; + this.payload = payload; + } +} + + +export type SocketMiddleware = (c: WSContext) => any | Promise; + +export type KeepAliveServerOptions = ServerOptions & { + /** + * The interval at which to send ping messages to the client. + * @default 30000 + */ + pingInterval?: number; + + /** + * The interval at which to send both latency requests and updates to the client. + * @default 5000 + */ + latencyInterval?: number; +}; + +export class KeepAliveServer extends WebSocketServer { + connections: { [id: string]: Connection } = {}; + remoteAddressToConnections: { [address: string]: Connection[] } = {}; + commands: { [command: string]: (context: WSContext) => Promise } = {}; + globalMiddlewares: SocketMiddleware[] = []; + middlewares: { [key: string]: SocketMiddleware[] } = {}; + rooms: { [roomName: string]: Set } = {}; + declare serverOptions: KeepAliveServerOptions; + + constructor(opts: KeepAliveServerOptions) { + super({ ...opts }); + this.serverOptions = { + ...opts, + pingInterval: opts.pingInterval ?? 30_000, + latencyInterval: opts.latencyInterval ?? 5_000, + }; + this.applyListeners(); + } + + private cleanupConnection(c: Connection) { + c.stopIntervals(); + delete this.connections[c.id]; + if (this.remoteAddressToConnections[c.remoteAddress]) { + this.remoteAddressToConnections[c.remoteAddress] = this.remoteAddressToConnections[c.remoteAddress].filter( + (cn) => cn.id !== c.id + ); + } + + if (!this.remoteAddressToConnections[c.remoteAddress].length) { + delete this.remoteAddressToConnections[c.remoteAddress]; + } + } + + private applyListeners() { + this.on("connection", (socket: WebSocket, req: IncomingMessage) => { + const connection = new Connection(socket, req, this.serverOptions); + this.connections[connection.id] = connection; + + if (!this.remoteAddressToConnections[connection.remoteAddress]) { + this.remoteAddressToConnections[connection.remoteAddress] = []; + } + + this.remoteAddressToConnections[connection.remoteAddress].push(connection); + + + this.emit("connected", connection); + + connection.once("close", () => { + this.cleanupConnection(connection); + this.emit("close", connection); + + if (socket.readyState === WebSocket.OPEN) { + socket.close(); + } + + Object.keys(this.rooms).forEach((roomName) => { + this.rooms[roomName].delete(connection.id); + }); + }); + + connection.on("message", (buffer: Buffer) => { + try { + const { id, command, payload } = bufferToCommand(buffer); + this.runCommand(id ?? 0, command, payload, connection); + } catch (e) { + this.emit("error", e); + } + }); + }); + } + + broadcast(command: string, payload: any, connections?: Connection[]) { + const cmd = JSON.stringify({ command, payload }); + + if (connections) { + connections.forEach((c) => { + c.socket.send(cmd); + }); + + return; + } + + Object.values(this.connections).forEach((c) => { + c.socket.send(cmd); + }); + } + + /** + * Given a Connection, broadcasts only to all other Connections that share + * the same connection.remoteAddress. + * + * Use cases: + * - Push notifications. + * - Auth changes, e.g., logging out in one tab should log you out in all tabs. + */ + broadcastRemoteAddress(c: Connection, command: string, payload: any) { + const cmd = JSON.stringify({ command, payload }); + this.remoteAddressToConnections[c.remoteAddress].forEach((cn) => { + cn.socket.send(cmd); + }); + } + + broadcastRemoteAddressById(id: string, command: string, payload: any) { + const connection = this.connections[id]; + if (connection) { + this.broadcastRemoteAddress(connection, command, payload); + } + } + + /** + * Given a roomName, a command and a payload, broadcasts to all Connections + * that are in the room. + */ + broadcastRoom(roomName: string, command: string, payload: any) { + const cmd = JSON.stringify({ command, payload }); + const room = this.rooms[roomName]; + + if (!room) return; + + room.forEach((connectionId) => { + const connection = this.connections[connectionId]; + if (connection) { + connection.socket.send(cmd); + } + }); + } + + /** + * Given a connection, broadcasts a message to all connections except + * the provided connection. + */ + broadcastExclude(connection: Connection, command: string, payload: any) { + const cmd = JSON.stringify({ command, payload }); + Object.values(this.connections).forEach((c) => { + if (c.id !== connection.id) { + c.socket.send(cmd); + } + }); + } + + /** + * @example + * ```typescript + * server.registerCommand("join:room", async (payload: { roomName: string }, connection: Connection) => { + * server.addToRoom(payload.roomName, connection); + * server.broadcastRoom(payload.roomName, "joined", { roomName: payload.roomName }); + * }); + * ``` + */ + addToRoom(roomName: string, connection: Connection) { + this.rooms[roomName] = this.rooms[roomName] ?? new Set(); + this.rooms[roomName].add(connection.id); + } + + removeFromRoom(roomName: string, connection: Connection) { + if (!this.rooms[roomName]) return; + this.rooms[roomName].delete(connection.id); + } + + /** + * Returns a "room", which is simply a Set of Connection ids. + * @param roomName + */ + getRoom(roomName: string): Connection[] { + const ids = this.rooms[roomName] || new Set(); + return Array.from(ids).map((id) => this.connections[id]); + } + + clearRoom(roomName: string) { + this.rooms[roomName] = new Set(); + } + + registerCommand(command: string, callback: SocketMiddleware, middlewares: SocketMiddleware[] = []) { + this.commands[command] = callback; + this.prependMiddlewareToCommand(command, middlewares); + } + + prependMiddlewareToCommand(command: string, middlewares: SocketMiddleware[]) { + if (middlewares.length) { + this.middlewares[command] = this.middlewares[command] || []; + this.middlewares[command] = middlewares.concat(this.middlewares[command]); + } + } + + appendMiddlewareToCommand(command: string, middlewares: SocketMiddleware[]) { + if (middlewares.length) { + this.middlewares[command] = this.middlewares[command] || []; + this.middlewares[command] = this.middlewares[command].concat(middlewares); + } + } + + private async runCommand(id: number, command: string, payload: any, connection: Connection) { + const c = new WSContext(this, connection, payload); + + try { + if (!this.commands[command]) { + // An onslaught of commands that don't exist is a sign of a bad + // or otherwise misconfigured client. + throw new Error(`Command [${command}] not found.`); + } + + if (this.globalMiddlewares.length) { + for (const mw of this.globalMiddlewares) { + await mw(c); + } + } + + if (this.middlewares[command]) { + for (const mw of this.middlewares[command]) { + await mw(c); + } + } + + const result = await this.commands[command](c); + connection.send({ id, command, payload: result }); + } catch (e) { + const payload = { error: e.message ?? e ?? "Unknown error" }; + connection.send({ id, command, payload }); + } + } +} + +export { Connection }; diff --git a/packages/keepalive-ws/src/server/latency.ts b/packages/keepalive-ws/src/server/latency.ts new file mode 100644 index 0000000..310d986 --- /dev/null +++ b/packages/keepalive-ws/src/server/latency.ts @@ -0,0 +1,15 @@ +export class Latency { + start = 0; + end = 0; + ms = 0; + interval: ReturnType; + + onRequest() { + this.start = Date.now(); + } + + onResponse() { + this.end = Date.now(); + this.ms = this.end - this.start; + } +} diff --git a/packages/keepalive-ws/src/server/ping.ts b/packages/keepalive-ws/src/server/ping.ts new file mode 100644 index 0000000..1a706a1 --- /dev/null +++ b/packages/keepalive-ws/src/server/ping.ts @@ -0,0 +1,3 @@ +export class Ping { + interval: ReturnType; +} diff --git a/packages/keepalive-ws/tsconfig.json b/packages/keepalive-ws/tsconfig.json new file mode 100644 index 0000000..b72a0fd --- /dev/null +++ b/packages/keepalive-ws/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "es2021", + "moduleResolution": "node", + "outDir": "./lib", + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "baseUrl": ".", + "declaration": true, + "declarationMap": true + }, + "exclude": ["node_modules", "lib"] +} diff --git a/packages/ms/.npmignore b/packages/ms/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/ms/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/ms/README.md b/packages/ms/README.md new file mode 100644 index 0000000..b04bd34 --- /dev/null +++ b/packages/ms/README.md @@ -0,0 +1,33 @@ +# ms + +[![NPM version](https://img.shields.io/npm/v/@prsm/ms?color=a1b858&label=)](https://www.npmjs.com/package/@prsm/ms) + +Confusingly, not just for converting milliseconds. + +```typescript +import ms from "@prsm/ms"; + +ms(100); // 100 +ms("100"); // 100 + +ms("10s"); // 10_000 +ms("10sec"); // 10_000 +ms("10secs"); // 10_000 +ms("10second"); // 10_000 +ms("10,000,000seconds"); // 10_000_000_000 + +ms("0h"); // 0 +ms("10.9ms"); // 11 +ms("10.9ms", { round: false }); // 10.9 + +ms("1000.9ms", { round: false, unit: "s" }); // 1.0009 +ms("1000.9ms", { unit: "s" }); // 1 + +// All supported unit aliases: +// ms, msec, msecs, millisec, milliseconds +// s, sec, secs, second, seconds +// m, min, mins, minute, minutes +// h, hr, hrs, hour, hours +// d, day, days +// w, wk, wks, week, weeks +``` diff --git a/packages/ms/bump.config.ts b/packages/ms/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/ms/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/ms/bun.lockb b/packages/ms/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..415b550801b6dd46b485cfc1bf491dfe8fb13e11 GIT binary patch literal 76713 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 a;~^CbD6>I`@{*h=T6nX`b|3!-{`oI68tw)F literal 0 HcmV?d00001 diff --git a/packages/ms/package.json b/packages/ms/package.json new file mode 100644 index 0000000..ee12b8a --- /dev/null +++ b/packages/ms/package.json @@ -0,0 +1,26 @@ +{ + "name": "@prsm/ms", + "version": "1.0.1", + "author": "", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "description": "", + "keywords": [], + "license": "Apache-2.0", + "scripts": { + "build": "tsup", + "release": "bumpp package.json && npm publish --access public" + }, + "type": "module", + "devDependencies": { + "@types/node": "^22.4.1", + "bumpp": "^9.5.1", + "tsup": "^8.2.4" + } +} diff --git a/packages/ms/src/index.ts b/packages/ms/src/index.ts new file mode 100644 index 0000000..e003b22 --- /dev/null +++ b/packages/ms/src/index.ts @@ -0,0 +1,174 @@ +const MS_IN = { + w: 604_800_000, + wk: 604_800_000, + wks: 604_800_000, + week: 604_800_000, + weeks: 604_800_000, + d: 86_400_000, + dy: 86_400_000, + day: 86_400_000, + days: 86_400_000, + h: 3_600_000, + hr: 3_600_000, + hrs: 3_600_000, + hour: 3_600_000, + hours: 3_600_000, + m: 60_000, + mn: 60_000, + min: 60_000, + mins: 60_000, + minute: 60_000, + minutes: 60_000, + s: 1_000, + sec: 1_000, + secs: 1_000, + second: 1_000, + seconds: 1_000, + ms: 1, + msec: 1, + msecs: 1, + millisec: 1, + millisecond: 1, + milliseconds: 1, +}; + +const UNIT_ALIAS = { + w: "week", + wk: "week", + wks: "week", + week: "week", + weeks: "week", + d: "day", + dy: "day", + day: "day", + days: "day", + h: "hour", + hr: "hour", + hrs: "hour", + hour: "hour", + hours: "hour", + m: "minute", + mn: "minute", + min: "minute", + mins: "minute", + minute: "minute", + minutes: "minute", + s: "second", + sec: "second", + secs: "second", + second: "second", + seconds: "second", + ms: "ms", + msec: "ms", + msecs: "ms", + millisec: "ms", + millisecond: "ms", + milliseconds: "ms", +}; + +const msRegex = /(-?)([\d\s\-_,.]+)\s*([a-zA-Z]*)/g; +const sanitizeRegex = /[\s\-_,]/g; +const resultCache = {}; + +function isValid(input: any) { + return ( + (typeof input === "string" && input.length > 0) || + (typeof input === "number" && + input > -Infinity && + input < Infinity && + !isNaN(input)) + ); +} + +function ms(msString: any, defaultOrOptions: any = {}, options: any = {}) { + if (defaultOrOptions && typeof defaultOrOptions === "object") { + options = defaultOrOptions; + defaultOrOptions = 0; + } + + let defaultMsString = isValid(defaultOrOptions) ? defaultOrOptions : 0; + const { unit = "ms", round = true } = options; + + const cacheKey = `${msString}${defaultMsString}${unit}${round}`; + const cacheExists = cacheKey in resultCache; + + if (cacheExists) { + return resultCache[cacheKey]; + } + + // if defaultDuration is a string, it's something like "1day". we need to + // call ms() on it to get the number of milliseconds it represents. + if (typeof defaultMsString === "string") { + defaultMsString = ms(defaultMsString, 0); + } + + let parsed = parseMs(msString, defaultMsString); + + parsed = convertToUnit(parsed, unit); + parsed = applyRounding(parsed, round); + + if (!cacheExists) { + resultCache[cacheKey] = parsed; + } + + return parsed; +} + +function parseMs(msString: any, defaultMsString: number): number { + const ms = isValid(msString) ? msString : defaultMsString; + const re = new RegExp(msRegex); + + if (typeof ms === "string") { + let totalMs = 0; + + if (ms.length > 0) { + let matches: string[]; + let anyMatches = false; + + while ((matches = re.exec(ms)!)) { + anyMatches = true; + let value = parseFloat(matches[2].replace(sanitizeRegex, "")); + + if (matches[1]) { + value = -value; + } + + if (!isNaN(value)) { + const unitKey = UNIT_ALIAS[matches[3].toLowerCase()] || "ms"; + totalMs += value * MS_IN[unitKey]; + } + } + + if (!anyMatches) { + return defaultMsString ?? 0; + } + } + + return totalMs; + } + + return ms; +} + +function convertToUnit(ms: number, unit: string): number { + if (unit in MS_IN) { + ms /= MS_IN[unit]; + } else { + return 0; + } + + return ms; +} + +function applyRounding(ms: number, round: boolean): number { + if (ms !== 0 && round) { + ms = Math.round(ms); + if (ms === 0) { + return Math.abs(ms); + } + } + + return ms; +} + +export default ms; diff --git a/packages/ms/tsconfig.json b/packages/ms/tsconfig.json new file mode 100644 index 0000000..7fc5833 --- /dev/null +++ b/packages/ms/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "esnext", + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist" + } +} diff --git a/packages/ms/tsup.config.ts b/packages/ms/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/ms/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +}); diff --git a/packages/otp/.npmignore b/packages/otp/.npmignore new file mode 100644 index 0000000..cd3ca40 --- /dev/null +++ b/packages/otp/.npmignore @@ -0,0 +1,2 @@ +node_modules +src diff --git a/packages/otp/README.md b/packages/otp/README.md new file mode 100644 index 0000000..2b19328 --- /dev/null +++ b/packages/otp/README.md @@ -0,0 +1,88 @@ +# @prsm/otp + +[![NPM version](https://img.shields.io/npm/v/@prsm/otp?color=a1b858&label=)](https://www.npmjs.com/package/@prsm/otp) + +A simple and secure library for generating and verifying One-Time Passwords (OTPs) based on the TOTP algorithm. + +## Installation + +```bash +npm install @prsm/otp +``` + +## Usage + +### Create a Secret + +Generate a secret with different strengths: + +```typescript +import Otp from "@prsm/otp"; + +// Default (high) strength +const secret = Otp.createSecret(); + +// Low strength +const lowStrengthSecret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_LOW); + +// Moderate strength +const moderateStrengthSecret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_MODERATE); +``` + +For each user, store the secret securely and associate it with the user. When authenticating a user, you need to reference the secret that was generated for that user. The secret should be kept confidential and never shared. + +### Generate a TOTP + +```typescript +const secret = Otp.createSecret(); +const totp = Otp.generateTotp(secret); +console.log(totp); // A 6-digit TOTP + +const totp8 = Otp.generateTotp(secret, undefined, 8); +console.log(totp8); // An 8-digit TOTP +``` + +### Verify a TOTP + +```typescript +const isValid = Otp.verifyTotp(secret, totp); +console.log(isValid); // true, even if the TOTP is expired +``` + +For strict verification, you can specify the number of steps and the time window: + +```typescript +const isValidStrict = Otp.verifyTotp(secret, totp, 0, 0); +console.log(isValidStrict); // true only if the TOTP is valid at the current time +``` + +### Generate a TOTP URI for QR Code + +```typescript +const uri = Otp.createTotpKeyUriForQrCode("app.example.com", "john.doe@example.org", secret); +console.log(uri); // URI for QR code +``` + +### Custom Configuration + +Customize OTP length, interval, and hash function: + +```typescript +const customTotp = Otp.generateTotp(secret, undefined, 8, 60, undefined, Otp.HASH_FUNCTION_SHA_256); +const isValidCustom = Otp.verifyTotp(secret, customTotp, undefined, undefined, undefined, 8, 60, undefined, Otp.HASH_FUNCTION_SHA_256); +console.log(isValidCustom); // true +``` + +## Error Handling + +Handle specific errors: + +```typescript +try { + Otp.generateTotp("shortsecret"); +} catch (error) { + if (error instanceof Otp.InvalidSecretError) { + console.error("The provided secret is too short."); + } +} +``` diff --git a/packages/otp/bump.config.ts b/packages/otp/bump.config.ts new file mode 100644 index 0000000..05c9a4e --- /dev/null +++ b/packages/otp/bump.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "bumpp"; + +export default defineConfig({ + commit: "%s release", + push: true, + tag: true, +}); diff --git a/packages/otp/bun.lockb b/packages/otp/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..b56486d4639adf46c199c71f984bfba15a0d417a GIT binary patch literal 106228 zcmeFa2{=~W|33PdNyd;NWlAJt$~;d+gv|3S^IQn23`MD=L?Vh5k-0%gks)Qs5Fu0M zGDMW&tmS$3_uaqudVj_1oa_3ZbH3}cyPvi8y6?|teb%to-unqoc0oTMZ$TR;cR_pi z6Kpnq?pwgY@9JUe;^5?J&+p*j?Pl%Ee?n*rJ_dss|EWmy+QPV9$xwy;;|X%5q*5c=oq}RZjDrI}27uZ) zl*Azy4ygdbxc&l-rvo^Q!xkKt<1h<{@i;t(Lk}F90fg~AjKhOCtMIxA%2&v&XnOxjEVLJGg@|t)2Jvfl+rbBTGyol4J#1k8 zp554S^bR1LFRuVX{wO!ro|}`qlc%+h&ofXC?WgR)w&y!QIFIsh<(GJ{{7`_{q z{tGEX?g6$QcJ{@f9QrK_>O=qd0K)z^7Q*T{dw^>07z~V?O#rYd zjP;iYAhc5o5Zb{P!RBqet!+WKU;;oMmX`yh1W27RkeE8S8WC@&y z^;1DP><_4^$3~&$U1?nH^_Q`tUDGG*l~ueBgi^|tVhUthOA4-I)|)p z$hw29SIGK;tXs(XhpbD;`iHD%$hwHEYsk6;I@QO+)f%kl9xhIH*52Os)^;F;Z7>*cQ zS0_IwfXI9a5Ecic6^w6pM`!3yKkyUUv+;BDgbRv~z1s z-VYRGj)Oe()7ghVz}n3f1DfS&?d@Z4?_+E2YHeq4D+~rNNSD>I<1_*w9B*!}uFxHf zAGij?WNqu=4IKe-cChjA$JlxJ!etiYaU44y9i1HQeZhHOYiJdNX+45%FS70gSi8D9 zfsF#p2f8}hAmwf@u$O@DagUSkfC^S|H^{ej^7Vxd+yFW-?soPLez4rx$rpwP<82ED zB1m>%_W>>Hfp)@mR7o3aR~(0Y0O5K7+O&pVc-Zr&b+B={{8HIvd>}t|;9?HPoJ; z9QL?TtZ|oA?DY?UbI-YqIX-VM&h8Ko3u1dMPnyQ}XhHU9GPP@ddSNOWM zsQvOG9zrK-HL{=nPp+KZ9{%-wkD2wmx``vCXZTw0dN;Y`oB3Qgv{IPP6=ZpI7Z*om zTQoJNjM7Px*^;JIYS!00qeoq>(pKB9K2^w4bu#ys+ml4GU3oM)=foRb*+#w{tTgwN zS%jXojM|#i%!lp@x~G5bn$!fR^QFY|HVjK-RJ-Wr_tNt0$BQ0F?KsF|J(bQBC3gGL(^GysOXP)wwrkfRs&s{` z=e=+3={|p$f~vnsc;8a}5^La5yrH1hP&pDE2k-N>c6vSKsgd8$Fvp(RVH|Q|jz?_I zBXN;mjjCT1c9YI2DZgH@NC**4s#}e|yqr|RNi!xsb?PW3b!1b)ZbEaRk??KAzRh;7 z?`H9J62E?xztgl*v^p`fg}H9`b2ED?E?i>pr>i|F#fa&hIcMV{ zZpUVp=NU0P8EkCKYW4lv4p05}M#F~Qr_*mFPY|(LDV#7AVZ9WjRoCoOzS62IO;%4j z%B`+d_qI&e=@tG;%N6Yq*V0dU1=(-iVt(8`I-{Do&#?0t?O3}!eeX@la?;=}qlT<< zcP{=!_&n1+-+Z=Dm9%N8-XFi*Cu%5}WqFAPFFw~?nUg#|X)I-T=sv!1dz))@by3-5 z+zWdXbFSPAbUjsEI^B(x*x))jr^2|e%K9;4O|eK znav)}w0wCfecs>h7-cnu?uE9Tokr?g0=srmzqRELa4@{|mDyNJ<>A)0l~tuyQ6^ei zS3ORu58>2iuC8HtNmH+bJ(Jqsw)hZV3AVi{>AwrV-$Li>S5M>euS7{blRSnxA2iKp zYWpgfcN0IiwECPGu;Q`*4vRwa!`N{K`sQ%`mIqsaHQ{t7i zxgbVKWd1IfLn=Wp@8!gTO}FJXQqiM}uZl_uZ4&)=I5qa2x0snFKM}@8w-_7KVRf3- zef7h;!55x4W9E8=ykMYmx`X;^?hayTQxtJ6n*23B;ifgyqkYtYS$r)z*u#SXuG() z($3mmpVw7g_1C{0&-wIxg575y^O$aRZHIk^^W<$ZI$rxr@BTEtk@5Ve@rkTQ$oxmv z31q!N)&peS5uph-=zFGXgRCRSI)SW5$a;pXOUOEhtZ&G=gRED``hu)m$ohw@OUU|% ztY^r&h^%YKy7Wmd`1WG>%Y%*;QRXaig3ma1#WS&T=tc(-o8OYTnC`Aj_4vN;rN$_` zi^T>`pOU{W_$`L%kH;3Vwx2V-*;{j7fQTSbFN1;G^#y^?fa(XX%j5kI#X`USV(iN- z|5Bc{Yp`4AjngEnMXpm;>)UN*3^I<5c0rNOT(t*RQw8ko4@45h@^ndcb+wa6oO{@p zMzhr-iS5figI$zgoG6)#bbQo;wx!xw^xZemsn@cmE_~C@DNPa{5E(INu6=RYr*7{V zL%!`9Mdl8Sd%~Ax>h=;dR%*SY|#k<+~_*7q8 z4Jv!8mt0>hYnu97wl3f5e!VA5tJH9&!Oqsv(T*;gy!eRn3PU+}CoMTY8~#xLym85? zm5)Cz+daE8IL%yX*&oLv9CRwrEcW!N*01Uw>bnEG@WX$MR2tLJ?|z&qnc!fq*mP}t zGCt1bhbum^?##qLZetE&pGo>E{v1z#j0Y(%|K;%PL|ICfTvfZ5JOO{2S=G%myJZ`P zBfWCu>z{=i{g6pJn&ZJw8fLIZ_0=$S!-b?aLqdbC75nU~F7s3=uY}HzzOm)v*{djT zw%zMtpw^*@!np`@e|2Ou(1IwI9~rsQm%Jhw+1Ukr@8(R{uJK*yjMx zouU6o9>KqA{_`iKo*m$CCjNH-A3c6I+Ws~if4y->T7&q%l@x;!0_}%(|4sZ10ACdq zKd7;ugxF64e7OF@{J*i^3HahT|Djz(_fI|`^=QEZ2Xy@Z#6!*_e0{**%>20w_;CD! zWo&KkfZ@EZgxG%x`1?@tL(2aog4A08e5C)O4@miX`3hUG@rOJ(2G=_d5jz2ZF9!Sv z-H3tqdBL%sgz(=0z9Qhmv4fOtgii(*Y&d_Baf{%eG!R?I03Yt3khX*AxUPioV*www zACkbUmJP3e*T6$Fc>Vpi{64@3kM908pPzDb{D4jJt2W7}0S}4c{_Eey&lK3r0*`$4Q@Fkbc%%5VwH{3-0D8QF(1OXr3H^99o*bc2LA^T5hz(?+1z^%Z#6T&|M z`0)M1^3VE-8(~Vgg*%QB7hI?A7I+3{gdGPKw3EaScUcdi`cIM zd_}-t?|nO>j_^sqLnC<{f4%okPy^ws0X~dBynbvXA^g)g`$&Gh8c6+ez?a3@UvKQ8 z4TQgp<0E~)-WVW!JMeuk*#Af#!9TSFsdwXV{6F!K^9a8N@X^mdkn?{+gwMkEKcAod ziHDp=_~w8Q`ya*)#(%wifbg#azBJ(f+xgQ3_;CGLZ@&Q-*Ail%oE-g$)hKMD9kxcI@cjq;;7u<^qKT*yas!N=bwq~18-ivjyE{+z#M z{LhmJUmK8g03V)(K7hvo>q-be74YHujkJ5c{fO|}0UvpOg{%SVaRA|SbN%)HXuY-T=>TFs6!3+C zeef7`2GQD z|9X8u`22gY>kk~eNFN|={2d~Ed%%b5C$jh2X#8^kUls5Xy^XdXp9g#YjPMYh-}w&D zA@x)NA6ft5*xP9QZUep|@E>Zz{#$QC?DyjAlj4X-`JZ%KLYsh z`az5%BXRtb7Qz?U``7gcayH6$27Gy7AI2VPZZ!UH0p9@d*UJMY))L}B2OoC*MdFW~ z{}Up7D;$3#V+U#@{4Bs%1pXuWjmGZ>;KTldJa|Aq){_u>`@zKvjvv_m_1cE|2;U9x zp?zfiSdVQq{{fEwZ^z#d;L8L1aNU9aBYr^}ze`BH{rrDDe?!WC$3xB`bx#65ynle} zA1p)4k$V3dBK3-JeAs?iw%$HK__Kfy`w#Xz6@n|5m_1g7Y8hBf4l?|MVHD#||DI!2K)IZUp~{gDyk(I)D%N z-!Ko?kd5*$0KOv5|BbA{i2ZuNmk0dyUN@08AbfJrczFN+Z|9E<;3L;R*mgu0vGczn zQZEVck@ts?ztQ+L0zQ0xOuhjw^a-)g1U7$g{=xiuZKL^yfRD5v_CKNv>-;Vu^{N3M zj(=p`MbDx2@X5i=KeP|^{>}JP0emF>&>qxSPeS}Z2l(juyWSWg{06{R2L2=CccXmf z{n++zWbVNBA@=nEAGRObMb^NLwEsNd!}0TPuOHQbkG_B0X#9zR^DzFfJ+KWMjh`am zBlizbW256Q4)8$}*5XI=FYLboeAs@(Z)6?-soy4~|CRw?1=W6J9Q~FIP9l6Quz1Mh z>?37=;==Pt-P?e#isEmy{{{db#vjq!sQ(-wc*yfF=ob=4`1yAUv1beT$o2ns_5Xej z;ok;)&;@JbAGRGS|9kzv=aG6%fDe{{f4%=E01tm4ADMp}ZT|tlhu1&EpTGP0zvYqo z=Kx<3$6v2)sEP2a0AC*Pk#WCX4TMhwIv=h-$hceYI*9O<03WU&|MvQS8t{?p4{X;) z{eKGhUllH~I;=P&pg#rF%#(y2) zt8b$J>A>b?Gx0YA{LQpKbCdR`0Dm+6FDn1n`xAIgK(51ZKK(8s*AHj;zxHp)HTZWt z-XR7ODbUb(4X~QJD~>R|7F03;}7OH;se6}2>9^&54q6(dNmL} zjUv|n_3pcYiM53AZ2%wqgu+5twq6~Cp99En{|tSHTtaZHCn5X^z=!u=RDcAzP-i^} z;h$B){{Jl`zaB2EgVg^3_(=aj+Z(mN6AV7Mej$1rjh`LhD*!$%&h|$8KL_x^C2TGJ zh!3!Re@aN*F~EoWU+6cqz0vk_sbDbjsP=D^Z}&I;pZrG7BmUn5e7OH6+<@;;1L02r zKD_^c{EZ}pF913p@`*M=26Yj>E8xTV|8M=j2l%l4P;b3s2fB*b?*x2!{|WO*89e`| zgw)-thUF9giSW1c2wxWP;rN4mr0j21|1pcy^#^?L2sISxzxBod;b-Ff|F`|u3HZqT zgN|<0J_p!*!1%-WC(sAPFR1mqgw#6&_;CFt0hM74H)_8K@R9KYV~FVf&Ne)U)SCl* zbo&GzQBlqvn=Z)Im1uj15>pv0~ zsP(&q)H4Kp@Cs#Z{olwt1caXg_@E2d_`kFF_j3q;81Uizh3!Vl{$BT=@yc4|2g2p@dLFX7q($N39&y7 z`0)J?H6TIXH#&c~!OaI;KcH=RY;^wEq4?0Jjq>jUK8!!Ies45>p8+3UKcVgQazWMq zjJ2Jbf6X80`+BcKNc;@|ADRD9cfASWU&Pr*{71_Fq=VFZjpMJk-w|DePYfP@Ap384 zZlmL0630jM5dA+PQr{i$!7IQ}SRclp9DZ0wLij0w563U0!2aK8|1|+VSc2BZKjdz- z{VYeZ>ks5Y`y1sO06uI#1T@R9u+By4p44gh@;~Vy_38j$5f^{RLCV+5->UQ1?=OIJaJ};o zv7-j~@cPZL0WS0n;YS0$0^q}Ovke^ZI<(#duQkZ^1|kzU*uVk%fgKz$L4>wAakv{G zOlX90=UFSoBCNj`mq#P4#}5vu560|Tf(Yvifdh^muokVI2iLT<1QFH;YwTKr2))0UslAexecf-+5g5KOyYDXmG%K7jg97NkJ+fQp}|0lxs_2bGR!g|BtfN>ZF2TTy5{umC&aX0}GCN#o&li+~( z6&x@@gnCopfaTNRfC(Zjp9Kdj{|*kA&xTm&Kt1uKb@6_P;vrJVa>c2rmCm2v2F^&ZCh6l$+qn zAwv7+0O9r08dts^gmHHQ_2Ino00^m`IE25K7bb|%PajbIt(8K?52MJH?bWp(YM)a%Q(li!>PYJT zUeuebcr=1D@PRYEGE46rErmiSKQApIXQjBGQ3u;NzW$Q>{M1N4I|-!=_i{)goU#`d zZ|{88&ly)7U*W)?aw-LlR+Rt!!I(2#<2TKz9l=8G{*8Lm3il08w&>veTbHio}pv4-FNZ8 zsjDbmcn^vs!rns(g#@49_qVV|?Tq?)^X|Kor#0^qh28$rcb_?d$WTe_^?2~{o*#iv z*{sMCj$NAWGaU2wo;GcMaLzYip8%sRN*C^_kwjSfQS$f^5my(JmrnUhOH)Z!?=JF^ z8?((*LM3Q2^VB1;Zu2-xTh zwn->@F=`#>dtl2MaHe_7B4vuJ;ZaJ;$Oj>JGZgt#9#?T(f5E@Co#jsNe1+6P|NSiW z^!L+=iYVPJh$uipwHk7jgBEqS#p+z&ucr0KFP9cNMr;%JYI}6=n`Rx8ojj+$dFjk{ zU-$2N@dxh_2z_dlPy3URiGzKzVSj)?c}MU#I~uYV!L%YM8rPln`0IXoOq^Z9cEFwTM!v42ZIABP)5^>q0*#=gV%m zrQ_7sQ{y9pm@Zunr!&}lJ?wmgYcG-rYfF8NCQnZnp4q1o?=a2!m4Km}_Xn4;LAO+U zo3FB~NYeoV6aSVVqetBSJE(k#V>k;ZRp<_Ebixdmj&HR&^6thn z$JwD%%+4S3;$)`!Sk0HMx9CXUCTpp7w^X0RD<^wYopj*D2UK;wWKOL`c7{3Z)C*B_N5g=7d}2z^~mmb1&zpR-Z3@cAcVQ;*A#ntp38(iCe`c z&nxX_rXJr8tB)mT`>`X{t;DZ)R-Wnbzz^%_$8BP}EAOFn;WI%b5&A527gMvB#ybU@ zCZ3|px}7cTDEpwE?3m0qaeny|w^sOjo}LJnlo8cA!xF`C=FSSeI01zv`<}&qH`$t? z;9B@>28jcFwt^(Wtb{MroyT^PUe@n6;}ZHbS9RQ~$E$xT_oYmg2(Jgv%zb}0V*;jm zYdg-Z=5)d@Wv$(#!tJ?7H(O6|c7!OL91+B{| zUaCT}E96e{0VAR){PbwkeX+VM_W~-d--X8dRojFHp3Qfz{W9}aYOdot^;x@$q|jlv z+5^v-_8pD%5%XyyMCtB8>xz?^F3Prk9t)Ntv5Pq_V9k8r>!a7;&CJsTp}L2!Z69gy z4cf1HZ=(OTJabb0_CU(_N%A$qS+XSOx9V|k9qiwO(q%>KMkdG&Y5B6ns@xZ=AM<%} zDI+Ig;ob7`rHi>ER;=ePOFX&XywWY_Y;wcd@wmKe!PlozFDi>fr|m?DL>8F}2I1dW zK>BSbTDMfImUl;(#!>aC(axtK!zb-zmdb`AE{NybE~~`7@|WFe@NS;+_Cry9pWYUq zp+`h|Dusk`f#mWXIXOAW7yIo{y1UT2{9O$FNw$sSf#0=x%x`;ZdCIs8XGXlp7p0ey zzvp#dj#QJE{fR_iWKi=;*XQ?9SqH_gCjV>;@Rhc+_;4${unDEhhSrr@mLck4YMpX9 z7VZ;E<7}|yXhZ6)SNAf$>u?dt$S;Q)%IoAj-DlF;aw3IMSR;h&X8M@zE_}PKga&6- zIPAB>XT?Yy*wMO=ov7Xn*@}D&9jTiC8Y)glqr6W$!7=QK#r+5g&)o6)V?Pi6C_CCz zn|4D^sfBO%qnm{no5~aYlwZGien46^vJ0ilf!6hvY}BL~9ptw>V)p6qUEvt5+!L|6 zM<>f~Mk=@DJ?`S&87?a$b!1h~x1=G3!*DuviuN#%U(BgE+vA;W!`oNky(sh-?om0> zy2H!_!KYIMOFHo+I?vi)PS10z+|~50C_||3;SRfJgZ$r0Ss8aeAzENHO1Ql+V0z?o z!p|PfgYEVWcarws*xsuIHt9$lxX`+q<8&cNf4{&}t(CUb?aEc^pBlF6eMy}p`lvYUM(aj38_bwC6DYUS z-n3DTc>I`mU$Wtn2H%(MBJ4LhLwB0SpM1}IWlxBgw?3=Tl<(f}oYK5SyoJ57B#Ih( z&-Lg;P`cb`U1y&;$GXQ6s$ZTL5Xs%VRx&}&>o3%>GJBLg-Ro1Q2Up_!4_}M!<&TUR3KOTt7E z3SEA}LfR8;p^0;^LT0_(=dSm%=8x=A)~PwGUh#&4*fc!i%rqWPC(2(Qv~D8BJLxL_ z@Sw+)i3HV?jea>^r1v;}PF-`zrTw1rI^&Q+M^K$b4y~wxa_@lp4{xKY_w>Sh9{fBO zcSx{FFXK$sTEDIR{lEY5d(sKnGvD3(7RB|b#9j7;uihon`u(j0bCh=_eege&`xPl| z&AZ0I5b&lm#9LxG{@WqvFh0_^=DH(Ss{-oq+J;`#y07_*)kWUdL$?V#P8x1~6UAqf=kd65^sD$^5JiC&~X<6)`9*1RPba^1k`(Q@;+U%kLqj0U_TUT1_eE{Lwdkq0Z6d)nhmiPxrIoGam7(4#Zndw&+37{79 z{~<0Z^=ZfTvA$B&8*P9g3_Gi$}%GbO8*IR&qsyV zYr+`I21yrl2k^=TcAll2A}c9hq02rx=&=>=c|n64{!fjMo`eHyx`b=G`~RZ~{JoH$ zdy9Mj-UEfgN~;o=L&uP9n~Pmi4_#dw-!yUyF5&ZpIoUU*#0nFTY?y~5SHDt zoZc;WI#%}JU5bp1`&r66l(+-#X$vJP$aM#`wpq>_li_sX_&tEur7FIxqC&w+?YV2m zwCc~G;fDj-zAC1Ax8(`z%z{%hDY)=%%!NHjYq5P`&5#tYJk931>R{h?z1Zq9(aq!B z_uxB7JovT`;6b!*Dd)Eao>6Hpj<%YF6Q&h6UQJtAw7CxH5mNKKbh*=?ujY|U`KB_* zSv#aHlH!G|Wz5Rs43(dE7sApXkDt!n@4co=x;B2r(7K21tF~)Lf27wGPIq0PR*WaN zwC8(uwt02QW8xNO$Cq&RWu>njE-wlru7(=lq|?nxq-d@oZO%Sy*(83UZFQH`nlARM zP~vD^lk>&GLG2f-$h|6sM7%#mbF1}xo)@`(1;0W3d+JV+*(6a+YxN~M!Z4~}-aHb| z8@$7gXDKI)<26c4_NPzs3*h{P<4gjrYqQW!nbwp!HT1EoN&Fyp;*N#vAJ=)KN7JWo z@^Y%4bE)&qn=bCNPZdf{uPfVCNbFz;LK zMN5hsHrH7$P;gA#5VoBEcq2J`*Fz_sr^21>MLkzrY6T0N&x(Z>&L7Xo&`^r%>YEW& z2{9w4quWsxXN!AxOBxCUk?%=CZNegw)2+j`&-Yi3Jr1wXs4uE1f8AuJnXo6}l+2U? zL6Eh0#j0YQe(+3CRIZ4+-YmPX#K7Ap=Epd{S(xI)`6~~!q@f@#5sr^=-ZZhS zmsdL$H=6NLAo6|AtAP7C3;Py7TvqUCE6hEr3_ z?L5x)6KCzk>B9Je|5o~+MA%l$s+E-6;-Bwn9zwihya8X)cRWINi8Eb!rc?y_D#ylO$_!GT+-oK;!xUE=Jx`VD`&3bK$UmF0e=cm>VuIwz?I%3H zRI9M>#6#EKgMe>t!f(xA>~k3T|EL0g9Zrbs{k21j(dwj+ z#3K30jN@AUA~b4ostxxtOLt!Rns+HX$4vJmU*XdvO%eUW?OQClZB46|gjmUh`6A~^ zFz`DO;D;2(R{^a{mgli-OP#mJe({;%SdnSNPA+`WSmMv0w`9!4pADQ=-6!9c{yvE^ zGwMEjjulmw^IiK?sdECSw)+CX7U1UfX z!!2xbIs1mr!b7!H0}}_@=6JVihdBC#Y&>~;>zWk3_1shj~VVu9AKvM~= z`}+9#T|H{;2c&MNiyTQ65vQtYx}PUn#VXZ#wOqBHr+e(VyD3rUv|V86r|-305foz6 zxAzM_sh0nAaymC=#1cD?F{E(5DWi3LC-FGtR)*s3_!C>tYEVU_w#D;w#ducx}&t9TES&AuLF z{H_;S6$?8J53G`~-&E1MH(D9$$4UH2PgyBWDdgJ6yW5LT}v>db;Fz;`GR#d=;#}Yr1M^U1HL$w?EZPN_Y&+-^#(C za{1vUsM$>mIQ4#qFR&4|a)Y4 zH9aGld=6&(6sl|NxRxZZ?*5FkzIHn;cHP0w?AnnfkSr?AN2|>hPCW9Ks_r18IE+4G%R%7+%Zp);$oNeV#>a&g#5t zQnv0zku9hElV9xUJCVK?2kdoI1FhTHVI9azPdg;#NMrc4;FV+-@7`dqmc5DATLm#i zZZ!2(VKphzn3p+(M{E3ikC=D;YP2dN{p?KST}t&Kb6zcRO&7Z_IfB+*y>CR`{;_D9 zvH9oz^mnFj^p5tKOj3_%$~%1DMcyGs^y&k(=}Ss8o`?_n`cf>80md{kjqNXgxFyH+ zVh($fQKNJ<(YnnVp;u|22=~)@h%!EZ)R3K!lB#0kl{un}Kj)srY864_{q9sW2mSZv zPtVzAD7+3bb1Mo)r}A=RQg%PMl)!-9_hI*`T4>#Fimq&N%xor!x%J?UQdVWf^Vt#8 zN3YCSV#c2cLFkw0D1HZYc}G+%D+l%k=_N%PIC*3hrD=$X>)Q+Ph`0$t0heY<% zpAD=&fUnOvb5M#=FiT!b=%l|;LW<$mF^y(5q9mS4u{^`LHC^nwu7lR~NZbAFeNs1b zH|5a_Vfk@*B(v}EU&{$R_6m%hT%A6pL#XwOKrS^#*}a)zAbYfuVCceLZ~Ain6Q-6o zKF!E}^g`+CqIFw7e<(}8sQ*ACk>S~mGA^o`P@zX^4d!ykA5<1OESL8@X_a`t^wM367z4br_7I=vkD15SRBTr;P6(dGk4Z zW=BWAU;b)SB7K5D^kv(lO9q!k%l1)~QFLFw_abEMc`yNC2%!$&V+DJ01?)J(#z7CQ z%QfFks!V>_OoEx$>Aa_KPnrkAN$1+f-j6B9v`1%CNFw`)8>}@LmcNuLiDo6U3Lbn@ zAoNzkt?GE1NVIdG9A42ZjFd#a$$O zPV!lzbPdtEcqf@WvS#1q$!lvB?ey_aj2%1qgO-y(JEp}zO)W>(J->Nx-Y0i9i4*53 zy3DDF9U@6eJ045^62G6|PDf;Kf!*(5<7ODd-cKm*Ad?Lab2ALJ*C})-!t7KmzGlIrjp7ehHs<%HAd@(-&3wj zd|s_2MO*TjlTGyfXWP$sQE{(tU&lKbGjn{5y-@p1&@~gz(Vl1(8~y0X4Egz zJv{IvV0l=50sTJK1g-1zd^K}eKxR=)XIn3oM8YxMy2J_Tt8#@&XTI&tzr#?<5pv=5 ziSQ&}N~VWZSEiI)=pL|+7wpQoT%R^yqgy@Mi}KeLt?MD3(E3gKRZG$NX4Abs=`X^@ zji!bM?iRVa5p;X+DgWkn<>z#EFQ<-jKZEvQtj&$H!AC~+)DW0AKXHz6Y0k_=>6)Q+ zFHpbdN~h9OQkrafFI6|_I{N-T+sv7Ys-okg0k(?g1R3O<+kNQkr=_@526vB)z3gyb z(N63<+u+=O)!px1^*Bn`9IZ>3FQ#RF%+%*w{EOYadrNF{I2N;cUj}9~GBCudUo1Y+ zezt(%7tgO8`oh>gf;S>2)?RmM+>f7ms4n_)i)c;1D@xY_t*ab#L?FdJDYcrF>GK?( zwK6A*6$yPt$cuct2=7O|z1g;B^bNEa7apc{9eL_J$;UahD}|jx`3XBQr;~aC*G}y9 z7(31^(YjZHT)GZA*|a-kcRN!`j$6+AJz}+EshIvr#>=PkBp@Ky;w`tQrnRP(f%TE0 zM;SuxO9}Tg1Sn#D*>rV@sX1Vu8)0>=(7NBg-DY4hdlS6a^VuY;;+5Jt_t)DWkqAAv zCrC{YB~9{T9WBfj{416rr|X3{^zYO+<9*EbOp0yIx;u0$Jvl@#HIi{SSkJ`K zGo1dOr_Ft)qbF}&Z8lrgkaNkFEX^YRz*B+JwMFX+_|kwkc5IkeD!hUjcZth-C#kO| z#Y^lyNcec5Du9|e)L$$x)1_g0-?RA{g~ltyk|>SP`Y+#-4Dh( zKS|s9idOI|Nb)Z&iUeI6d>By?XR{h?S(5F$-_k##r}<}5=!>5BJL_etI4-``WX@l9 zsyWlSJF1MRI0+A>Yme3~%RJk1Cnt>VgWuH^l?y?SP3l{EatUIzuEo+yWf3mb&0S)s z-W8gi%vgFlXp4cx^o;Ej{I4~+jX$LL=Jd}Iq2G%*pmjw$i{~rPVN|ImAK0prAC-*z zCNV#y;-ov;()6CRd)wziwTN-c>9{(Y=78!qLb?rvC1h;Bf|D)1S>isnlSD3}{B=a@ zI;$(n3=9){mXrC@JbL^5W8D2>d_tW^wqojJZqG+CMNy&?b0ozV7gdyu>{QvlwJgoM zhvLy1iRj^#sZd?!nMUazN9zhIWn~ENWsXx_oi$%L?OUoH(!)OdbkgTAdxq%1*+`w} zJW{gps=yzYNY8YN@Pz^Sv{j^4?r=4F{5qj^r&L=h`Cgpw9j4hQ9I#J` zcartlDU%Pj={twN9Ns-GBva7!>xjdZw;6Md%x`3!g)?XQbF2@iRo%O)^S)6wOcZ&~ z0KaeKjMlZC&wBB6C8zWU7f1Sc0WQ7vmSEoE6>Hu>y@Ejvrb7g(GQ8S3afb%qQSn%2 zaC=E~?vMGQG%T}6O!q~X=2p>AR2*E;x_N06yNZ&n&-GKeYf$z#sa)>=mD(v2^^@gT zTwM2Z+NZ(8((1`yn&#-HHBQ%EyS=ZC;(`{Qr!l6nE+tP! zgb{hE-%Iin&xkKZeK!d?;_1yKlw%f*NndDeZ>h(3AlmU`<>F5N7wj(K0&d2id@C&` z3+846KcaNq(7MW>w0HeHKl|%sF?}r+$-W|`Ck#7*&vRfQtF(@NirR;`HL_udZ*XWA0Jr;;&}sw17TQEB@2rwUP~OE=8g3X1bCpEtC zJ}^-2_B<;xr<#|S%3-jo%tRM?C%rG%&ciMyMeq^J(;?qC9%N%#)(WO&KdppM zn3i!hj+ig*rZuSF&HL$-J~gZ16E7|6%b(43QMz7e-EX|!*TD`luyN>MV?m zUjG%k_d@Q~Q_>H|e7e6)E;ZM=_z@=&9;>a;IVGJX{p}&cKv<5p1Leh%=|Sv$H+Eh3 zM(a+nEGe6FjWdo+_F$HljQET2gUgZ@P0YI%&vbDqU7wKJU1m+=f0nA_R?yd}jH4X= zZ*uwWRHX=7PpGo&KRvM%<*yG~*O%}c*%!^#x73eaAD@zOczfzt+;Z`*S7AXvTZIZ- z27W}ijNC1Jx;paCvZ+m4`>qSQ?F;SQl85&a_uNxY?rA=X()C5_@^{~vc~((VO=QNX zocVUi4)103S^uZ}HcR_dg(L=Q^l$rC)$3P`w%k^-|5a8}p6F4OJIc8?-6@~${Gv`t zRT)ax53OsSL-ER@N~4iiUN`ueP5*8Ip6ugaCX=RgOPLd1UU}kMK*eW0_iPJAc9M1D zCp)Q!R)p=Z&Qw_tKQtEOH~lrY1EqTct;^N8-@a;mOV2?)o3FMq`-8T=3L>vHb`K4{ zphUFwoZ*$qxJwydD!BOu&PzR)m^(S4S`~R;^DZW_$*HA@Nh%P%?>mXs%^WDuFvQbz zAo@(R6l{2T>$aHry>232aWqL3K0mga__lWXX*d=gKVVI@Q*}qf718ug%;d+wQ+XQW zDUOyOid|6t`lEGO?tQlpk*?md|4muYSM{BiZTmXfL`Ix)S7(>zU-F-74X&hRzV}L{ ze~)^#U$J6ik^ooR_4278?pyecL?(<1*!x)QI151QT6mJapxU{A6n`|G{7tri;y(KP zpo!@AlcqabKbaUDu{!YGqPXn{t9!)`y7bSy3Lj10mLv!owxxP;oeg))jX$R!o|&olhnBZr*ws|Kx}Bv~{@_imj>p zbKa63zM%Z)8V}N7i))J-JlUshrM+?@YflyJ!07ZCR9V zFj`lt=F_E&c4xyFmd3`G&ot%^3Yv@W`)uX2q|&%@hT>x|w`iiuXHM)7dw-d+qgUc3 z*{_#oW?#MC++KbPug!Bt-)|!S9|X!1TI-y@?8JPt@Y);7OL;d(NVYdu)_$*jW0CJD zet+;_>pLIWFMb)8mrm20zy4*ubMcbiprkoB@n<6by56ej$8+d?-)XeJwx{W3*gri~ zaM^OzQK{?e;edzF#urn4qV73f8%@p14HRQ~FMr+O8Q1m?hd$r;poy@vEN_k?op|{E zsd7P)n%*EP4xwn>hyB_Xdd=E4^ZQQg>I6y~k~8+#F()(fj1=hJdPn%-vDvnIF{vZG zC%AlSclj*eI4p9Ep*&`EA#BI)8^LDoj z^Zi7cgFI>xUn3^2Ypr(Fkfe(Q&a4!jih20-^Nif5*Z10L?S)=&jYo0&z*+T@t6*S%EkDUP(Cpj7KG9{$+kmmLRvlsDkkr z`Yo56WXZg_wC+qLF+=+s%srKg3=8`>ozJw`{*$;vP#PmEHpm36d0^!`0-U zuTsX>Eo%k!&9e1YtC6Md=0)j7pmlvG%07|b=zjG0K(QLf08_dc+eMe?W53P_{<`IL zHCW=rtGjP{KYtT3NLDzlmZDJ~mKr6Ib;fJ5O-7QC*aEXp9;F+J*3B1aG>R$Eu6sI~ zN6RpOr`YwaukE9|+nW~m6to}Xar+@v>~a078rQY9wvTEl!NS*d3hos!#lDiqbdpP- zd!b2&(mjvXjmk;Z*Olz|rPgHF+mWSw&hf-kt$JFcV|&O+`YzV6-eRvdVoH2J#r}@| zj_9^HkD3>{e1xfmBYQt4-`-pP#18$O>jGLg<(l+xe4U3~c2Fe;oe6z5MvY65x-f|T z&1j_2a-*mE8P@a3$+G6GJe(dZicQu`=F$EyD}KHMUuN3h$MD4p{k^s*wC+ytcVw*J zE>%mrjZp}+b2_FBR|hAC*bjh+oD%OgRGRXk{od5O2Tf65?vwU}eP_Ja zHja5tU2{DX6^CfFF6aAg*4LBf{BN?-iLaiN$Pw>7d#F!vckzwB^WlAADh1jjU!-cK zZ#nxupg3VNE6}RFYIIYK(PcP4E0Xyu$CwdH_aa(X(30Tzt?h>oAN+OcL3wR-!OhyE z$DJ7zl>23hDn{?Psb@PL*Z5}Zs-v~^g-XX_i&%lG<{4xEkK&Q{Pq<00w$`C^FQIi^ zdzNSLEEP`YMpHC>+gokSd_TK8Q&oXWSA6L6HOJ3KiQg~`6uMQp=aI5s8k${jd3l7? zpW7okOh97)f{=RaGn8%&T356`W0ukGE$Qs`U3i@*0MRUpW(VQ82zysk@r*=Ou3dvF1r!1vU>GSAR)xQy2A;=MB#rjY)n$3aj}yNgZf zy=f%t0nOKgH$PM-wUo~AUP~yvQ-7iNndE0Z1$C2a+o-J$e2^d1>iLrSrA4OW4janf zD`;I~jpwQqN;&7GQpu~0XVWl`ic-pm&kZgF>c3XFKpF8VI^g&PhI0}8K6RX{R&UQI zf9LbmdsoOoUCfmmc^f$ z{RiB;wyW*UJ+RoK&}ufNRYue`bYYgFH?>7Fzx(1@e^<~v!UpG!v5Ps>0EIH z_@;ETk*`s4xQ5n!5q(NqMsM!8Wc%rTVb3)rf5<=oM(|j$Kh9KH?CflATOQU;W699nnBfq0ghY>`v)wKM@QlBf6f2};r} z;4|rzEPrYcy<2K8d@1ydJ{{vgmCe?m6k9ocj!-+(u(wPECii!xchd{`qI9pLbswCU z;aPs-E9-nU_wYc8uamJzWAClL?;mEH8|K-DFYfeo*5Dw&>{43cD_cY+@4W41h_Bhg zL&+&liutsQu11b1-5Y4#JHL+YkJ_1C9%tbi@+0Vx{js2r%tN+IR63TV{?aPBBAjOZ zRc+ToXl*X@KfhI)+S1`!+(JJ=iZ}20Im+;E0s6WbkJb&03aJnhycexk*Fa>VP{aHA zMQt(P5r+>0`VxHQw~E`01J8Thj(I!R7I1~Nh_N+|UgqJ4F)FRaPtrWJ633pR_o+A0 zy1f_imSf(U7SCS1XRCd$b0F=YW6AY#JG+Savc2lOriFs#v$00ZxmUTR%El>W3*7Ci zhEX-4lS6VSRbwA+tQ%(PW6aqivW{QQP`@Ua-Dmj>@Kb=yT|1HZg( z4$5-5!NbP+iMQ6a{{hqP%BP>Hd=3?UQSs2rI@;E&+k=X4B3gHcYu=r~j$>>W$LjS? zrIMXJ=}#N^7!xxq+&(5XN*ohle3N>1`E@}0D&O*Jf0LZT$K|7q7j|4yAe194VKk{h ze;@o7S~oQ~>j|Fr%hh)|a@^lSI)?6hQ4kdF!<sD-*Pfm_eYKT2YRQAVope=pf7F{U3fDtwxz8 zJ6x7NN2>Hmhu`5BDWEZLbQXT~w7EQNRZf^piG}g;p>WcN2MJNSx6!)0H7EUj8)B>v z>amBv`yx#fR7qh*n8}(r{fo4mIJVGV-stL1{qEzfn5D+ul8f=m=DyKxd;)^_wpZ8; z9?;S0p>&hcy0ygZq2JyezOK{}%^_ei@++@*lp=^avF?xt>z%iC{xk93-JeV4c=%sk z@x566EOC)RE{x(Eo7AG0kG4-xomVMJHwCS0^YXzdzsP;LOf~}iM&bwe@b|dwn12)~ zM8!08GGb7kRn+VKoFkRIiDzN-r^&51A4UDrevu}_n>MqzL^h1R5PcoGgVr6t_A6R8 zewdxF_3e(c--X@sg(eS+g-$&6jdXL2diA1zETZJFTz~&kP+$p1p{VLghgJUV&lkDb zA7*1J4}Fx*L;0JE)*aAJ3ppft>h;X^z2-%d7W~KdzF<=}6rC(Q^;%hWp?%EJv4Wyp zQ}pv|jRD^+(h}-(FLMtjU$S@5BQ~fuzvz{W(oI9_&g8Vx5|Ejm4JvxmDksR+(qg7W zZ0>=l{BV1@aRLvg`0%Q4H5IuQS+pSwwU|`>(RcCtN!dP^56sIY6*k3cNuqS`qIEI- zY=<$)85~9x%N43#dB3hZYE4EGMf$kfE)4Zgwf+3@mi3!mNq#`LD|v^b+pDA>-?R2} zN+^HlyW+awBxsCYAMT-bt44KQ*x&SKSZymdh%kDd?zRxt))4>dZh7qWL`9{3YICYn z*3)8x^YhG)3uF`gVjO7f&csiL72eM!bm<$_MPG-~(YlB8Z(%-CjBB<=pMUMJBEvs) zSR_P*g`A}!i6>iKgqH8ozP|hV~T&#*Xg-ccA^fkJinjSL)Hf&hB?-WXH4VYp2SXWn4_OJ$u^-4MOoNUTv2+ z9P^X-jndNlBEOOscd~UYxmilA<`tezK0TeE`098=JIdcowC=HwZ)U4Yg5URCVfTwa z`aw>YC!g>9Wv;%j)E?%?NNkAAf?OtNd)m*A%xnK&?Y#$7Q^6K43<`>kqS&y23W_Ck z6%i0S0v1qIENFlL!H{4Qs)``RiUoUDR4kxkuP7omY^Y$vhS(LbW9R?ooD*_D2*`5qYl@^e8wmG@U!xZBydCw3mLwe$Vz zMHzkk;|x0$UkG>`cco~^sYWrH&*Rrdb-o*TG~wD8)52Y%%RQ_t429<=2KEeF<=|~| zDKpW?r0KW2EZiMz+-L5MFD#k*;^6cp)^3AaZ`yWJNqca7R-~iJ&!ftR-*AT2%n0Vz zF1BdySo=HYr}Nu{`}aP)eL3v#*R8&b=WQ^}V%KL$Y}{_o;s)(nzhv`QgUu`Yn)xms zQs=VSCpGm|IxX-u*3ElgI@QeYR#(TZPe6_Lx^43od8ryVeX6s$zE)b{i8f(2nIBpD zCbMw|*blW`yKmQqG&_yY^KScGxusEK?65g%I7q*x+xl7E3m!7%bHf~P36A(4~TB3Pv3A;bJi;Wv$w)$P7 zQ-en(2bLy({B7W~l_s#0GsO6IVcIE`D^e#}qrC@HK-E=4mHc`tD}q#-aftsT7S%~RukhAZY1_vYu7L3T21|JH-il8rM@tG zZ&#?Va{Nehe*8eqt!_ZmRCohKEAVoF6-BvF@C!{R3NEv3qnWLTcWY zJ-@z(jr;z|cHXL%%1e4Hy;sq@?s@O1jc%KSgKpz4Uq05b^=OY-`6k0Vd}wp2{@Eoh z2hZOAZU2*AZ;xD3d$efDK!bvt#ckN_#a=dUt)rr;St|E$87Cd#J!~voZJ8Ojq{sd^ zfmWZoy8{j5FE@2@@8NL&%+e&+?azNE92nS5o~=CoEG zV_NFW_FV5+=cku`Qx@)iHm>7>CYP5UD>|NMG3R+<{UJ-D<7#Y6^)fEBbGsIJXjbbc zTs8G2J65U>&G^(JMKtZU#v_Lx5$;`_55`|N$~rzXeJl(202?7M(?uPB3G_bW4b%$H#gzL*$}aLx|%Jf7vyH~hI^MYBtOvGhJ@di-{jjq5sGQj)#u z+OZz?eG1kGf8WutPyM$s&!SY8FIO-A7(XYh-{&S*NB$TayzX<$aJx?JzK`yuW>FZd zQ~R{Kw&&uG0~^v?&(tD-d5n##64t-*YlB68x5gT1tlgXVy-73IIfZpQo}K*NMK$sG zmB?u^iw~=2%xLU*&b6mxbF7tT*A5=J(J||1d~VZpLG!zNDf`m<`;N15RnN}7=Ju_x zQp(PP+ePlrM+A*k-leT#zu}VGQP+($(lY+6s!Ywbtm+!lbanwg!)9H>^2Y zrAKVon}>C5hfF=#e`nB*2gbHfYXw=gi!YAL?R?)jf}ZbFQnLf*2{vv&?csOpMlbJE z*G_48o;1JcB7br2_b{FCmC4U<2W5w@44imwOf!{EF*`h)o2)B*)AQoM*Xl-kj*c_0 zw0PO%an}^e4oYfzz&y#u<;QI>^GLL9IbnE#N{}RcTz9{#a*dc&J!Jvr8knCQ-;&?Mk-1KH;s+^u+{f{mX1>z+2tL` zGfJ89V{iNJpE+YXTe*dO8qsgdw6U&Xk%LqlHn3j%@Y?QgoC)TM9XoBbx14G4_WG&b zhZZK$>)rIe_)~0L{_Bi|?SyZ(4;`?q-hw)7#rcw#DQ9L4x$`dIu-doivld0kQ#Q;L zXxSCSFOH`PI2uEE9;dgDv^E{nLRI3jqJ~Gw+vJ%^<2(-?Gn#s^bp!u~!`spAJiSl% z3>#OvX==EtU$A3#TWg{I`~@Z1@q>z<)xKL)^I;A9Y02$F-F`e;Il7>4`yZa~du9&) ztS$Aqm*ms;?2J|Q63-p!F_HfMnEpQNEE{*-9?7MQo?~Mcb;(g5zMMC@?yA=Y?(O=1 zf9DbFGXH55_j>H^_vfBATa@q8s9*fF&Ph}H?U*se)b?wHOHt9W-nREyb~wkz9o5Rl z*wj#Umqa~eoo@PdQ|%WE4k!Isz9VbW(QWHLHBa;V$k*LqXXyU^@mN=z4r8K!F1T`G z$DxPn{5wmNEUsN{MB%DY_C3$Wbu|xZm_PH>Wc>+Uzpm|><2koyqxVTyQ|8yZIgMW%BZ(<$C>lO}c(v*5hujzxU*O<0)Kv-|Gc7Zpr=J>4H6B z!Ohgg9MhQTC&!0fF8(%LKYvpsM@ubRH7{Z0;`Y5+aF@;O-|IK9M)(L!arOvOw0F8Ztk!eHcoCtfE$E}gpbV6M5iq?x;k7^v9 zHL!Kp&o@gaW}Uv_)zdh@28;y3|b?CFCWVfkhYTBmt?ws9m z4nuNudcI=pKyWXyac8<&T`=yo`Qhc(!wsIl{xYukaePDJvLUs;U60j{N^#b7X=-XM z>=KakEPvVKD{((KMu`@S-ssNTpVTvH@a^+z(G)JdFa9zc*SDizkjm*1AER~Z)^N!KkhHkQ<980aN9^THmrA4 z5~HsQFt4z2Ju(hX3EVkjYR@sdg2x9qPIjLi(?TWbyjQqJpq9ZH>6{O{@2S5s>ub6) zz4xAH*^|~hdzdb@j2YD7X0P?C-Co~d{8vp0_*dDu>xNIOz1!tbR)9-H_<%DfU~=P)QLzV^>KGbuaJjq)`%?lIew`<)Kzxuy1x+IxKa)DPVyydNr4 z6TR0?@3Uaoz_=%|-SXxhPW!Y-*X-#rcMn0QPX7K2E*zeG@~)vn=iOV+xly?EcPQ7{ zxGzVKUOdWUz>YkXy4&>!dMfKr|H^Yd@a5&Uq`1@L#~HO>wppdbrD$t{UHGt-4^CR2 z?3H!$kWoaBf>!Cz=Oz12pH1P?{gNAO+;*$>Y&9$Tmd+jQY{-ckH(0cKh-SL&`JLC_ zU+!q=c5cVTgDbXlTbUSYem5*(?59OGf#Xj%$GBmOF!p%&cfJoUuyC{4xCJ9V-T%5N z;re!+PJPB+?`KypS@WH3L*ow1cRzYG%x=Z({mt^{nmtRkFnRH#>-K94P3xxKv)$#k zStrq2MX$llgY5aq95!zBll>p=c6irRxcz2cs=C|56bmiGH%YcT);84*Kdt?6L1>dF z_qTUEQE+%r?sbvgYvuaAJNSk7wJq|$Z)M}!-02ca-s+{Szm4 zxN)l;4g=;!xH{UL_1hNx@=c*=NZ@TO=?0n%Ydzz|jMaSxbh8aASz+76V^;dW+9q9C zxVPB2UZ=FUudOy)ug%O$2s(Gn>eRafdwaT#TGDXdqk&&~?!0g6(s4z%v3L3n&AffS z`H-%Ai-h*mUOI-3QZaw(Y}`68!VBkxZZEvp{_(kuak^oxUbcQ(vt?~9 z=QnfjjMR%+qnp^*{$k1T9_KA41sXZTn)`j-yS(09x0>r8G?&gdug~7^d54W_nBm^o z;bZN&;Trjo@fN`y=j`5?88m+LzMktw1hh_!tl8>b;=M*MmksTlGb*Lz%DH}mJ2@}6 zHg|dLHA~tw)#}qgmcDn{xH@khj}5-=;MY;oy6(V^3pJOHMc` zoBg70iFcv7b|c#>^K^!bo;$VQ=brjGQT=)J)bSrj<+E^e*|=>aM=d+sWNL6sk{{Zv z9lfgQZ2yG`4KJJuOp`tsuRi&GYU19l1}DSzcAi=HME~XCC7BEQuGwPibae9Nu3ffj z2x3{d_t>~OXOHEqXnSnaqIqF4Dpo%B8Q1PKUA!^eAiLgy5iajt9}RDu^r6p`uN@kx z?w+!_mCJUA{SipT_2GjYJGOKF@twV|?LHfqpP!r2V+~hy!e&Qg%KDky-T`%Tzi`Ih zU#BT9I@RONd&AvVuZ;ZSl&o|{+b-{|>FLHTN_yTtwB(CG^Y)bj)sO6XjR$O8i%p>m z)7~#h+7jHP-k^c6h4Y6WvkZxOc(Bpwp*GXHtz7MsGj@@d$_sAMogf$Sm$ij;m*1Vm zl|C&z_0GTT`2Dq4vFz}WjXPD)uieR%6w%~e#h!bQcpLlAH??@&s-=?I(Y9LodP(Qa zH>8}jsr!9EM&ChZ*-@YGD6j7SwX6NYV5^d7`%cFa=zc0)?>u7TYP1=8uTii2kvHrf zEiwLK(PPoI>th^3{N5$ps_m8;r*-G)!HogqN9Ad1+vGhqd>EnIXO%@Vzir;7S+l>7 zzOgNcz3!C9#@)2GUDoo&n~cA7-n4S^=JsQ^i^q#MCLQY1um3%bI^7buL-n1sEuQ-8 zCp}X?*{|KGG=rsPyo~;ZP6Jf=i`<`2Vy|~UX5(J3!TxAE8Ao$HS8WtDKzeQs*)%!i#zra6DFWfNRdC)hwgu*0i|kyU2Wo z_|DDqLk_g2+f5qx85`Gjaf)(wFAvwOyI+1x3YprrQ{N-e9uI4`c2%jx6%5w9)iNng zX}$WXWv7~|oh`^&9d4Oo)nvEe@ErTmHtKuD4VSa*@SKh7H+0y=B|YO}KaZ{5`r;z~ zi3W2v)b8aq%h7&cd@V2U%$jdn)M-5OzAkTPr(HM9zX^95nAD!O#JWjngdTAJatddg(}ozd%f=Y(Bn z-R)v!_G-OQXNgL4?(qUYr%uED-m`FDvT+BVbo2RXQnO*~v9M-l<9;kKej;qqV5iab zj_a;w^gb1_F1#s!XJh~Moqg2zHwz31((iNjLu2>zUmUbL7OpttQ1=ZB_Z1trq{MoK zYkUV?gN!b3rAo?9@t@5NB5#S-wV8jZero)-BN2-pFZrD8oH{A~g5|S&uI)E#)jvGy z-ohH4w6+WwWh}ci`-+$|KT-+fCC5pRO~w(4ge|_}DB-r#ALmMKv30+23x{ z#JzRVXx9z=Q(f!-ob0_kqhQvpK8x!W#f?SxvIm*`EAyybxm^k znAqz;$;w^hQY^Pv46`g&?wYOoW6(GjZXp|2f197vn})kK*=67Cv9wD;2j`?(28Pmo z?c1!LJFDBge(MwVj|x|6-0xk0r`7u=#;>k#%Stbrk#6s{MfAzid0iU=7H$z6H@{z# zr?Fd9Ze?v+v&h3YrC{DSr-Y@uL&AzvmG4B_-v8Kfk56Zz&Cp9LOheXAYoOgO`fR54 z+KA zvh8xbvtE})reRl}PG7LlX|HFjm-(AF+p@p;=DtqVXYaFl$Hp}tkkUW2zGqZDzcb?d z_tu*itQziRc}Op6`1~P-Ej?;`&Geek?&$IE>jVkm$NG1esrF-;{fWXp(VZt;H#XyZ zeZj7G-m`Hxj@sO7RNegxW;pdc=N364;G&@OPmA=4D$_g7nR?4^fXL`pP1hJhoe3${ z!7)Z{UDcizSjrH{W`!Ng92s?oW#5l%T*02$ zrLBf#G&Ng?1N&x2O^B*Bf28Q5rBU!kg*tnr)=1YgiJL}FqlXWI+QL}(X9G~n?fzN;KntC&^iO_W4 z-tR-?YHRoLeZerDsgvakI*es7pnw)XdP z+Mi$bi0hm(diy<{9;Z%pm{IJJ?a+tI)2wgg`gQ&@pL(|Sk92-?wd8UQU%x2_>`RWn z>G_Ghu2;;)O<&`-?}qQ0WwW)~4rsIFef&q}!pd zY}|$^GfmpByOn$G{l{8U)|$3GN z8}~^GYp?NkLbf#1-Pxyc;>S77ws<%4!4i!@93dXW81I#{tJ6VTa7I~QrswGsoQn;ul(fC_TTeY zdItx6*NXSp@pW!m9q;~MUboxx__@s!X~Ss`oCjO8aDTFKcO7u%Y1O+>|8*O?#wJsT z79TPGBK+}c(KE~BLhTvWm%8ah4|6Hpes1PO18&E_X3BZKr_-$JO)6nW{m4B4oKO=JxvOkr> z%awn)tlZzIM*H~VrfQF0?j1Ozz<+MT1!o#Q(CI6w6}9rp$~(hW?w{{}GDY*I_|fE? zn|f#5>kNOZaXy) zOkDrg>*3BP;+4MB4$Yl6D_deBeEKXm$|E?XC~K?H0GDkRn^T9~N^{lYt?{^br0#Re zE%Uu2YfWO=wH#>zk%Jw6h#AY_qnT7GhnE7T-7~9gTBq}a-=h@y};`BC)*zJWX z8+X+EiI#i%I6et;&D_6t*e?FlXld4=ghx;HPuuU=TKmpeJEP92E8Xka?z#6g?dkg= zV@@YbU7*%v%!k{6@;^5K-NO**kM3 zo+q?lh!=NgalXNaCB{pS^x|#|4!9BD%)usdM@Wv^%vEd5W0j|~aBH)1AJ!f)`=&T) z@u?eEns(`T1mEfPYw$KMz@Uw1o1^}l`(G@{AF=h$%Z1nOt?4Z^;)x$DsIfb0W0%xJ zyb&j3CJyAV`;%&HTtR)oz%j0kT{Q!0boervv!G+Nht#7pdt^>eFxVk%VIIO;cE>iByL%;C}b?OD@2om2(7GGb#wM*<$ z<7SOV@(N5BsEEJr6|1jrnmki=qV27PpH8S;xD&o*L~ZT*;)nH{^Oo1gZ|54ksHzJB4f?~=`9jy&sg`_YVj>LaI^UHvxw|p`-OR zEiY^Kh-=r4o4D4Dg-gz=mHnw4yr5ydvCCVVIW}9fGgp6(YFVe(`RP3pyz5qU%)Qqh7`fVKYFJJUX?zmu4f^Ky8*nr-R#?1VBDN%dup<||V)1!GCOCn~6PS)Gg zGWJSd@WVM*da!WGnvU#GWktUlW`a7$rcK${`dI&2SL+RV{y`FA zwMaMBrr^-4d-dMioafr~f8%p4!l%iLGkY$txY*WlW?L36`F2G1r;@AxDAeobr8Apb z7tY>#!yz*NG(R$T-R+drxx4fa=6270`o<(x{Zpo8tH^pmZp9Oe#h<40zYe@Ux90S^ z1H+c|c*w%lV&e*P($`#Aq}Q;4xyNbm$o5vV)ObU(rW}9%NXK_&+m$69TXcC=sG58B z8`tmFu!}FhoXfTdXsz>f#?2-z9;|M!cZ~R~%)SlTxVP`0>e1gaGODA~&%^sxoq5*o zyNcEK!`bQk=9r%j)&2Q+eX7^2WB1Q$HaoI9?wH5C*&Fls^F-J_aM=w4uNnRX&ZMI>-jksO@E;V#l%nn=VB#OP~ zQvcV(ajm0H_Ruw3xc0~Rv+VgOGKVDlQ)#{Q+q1ZV>rM{T75dKOjMlkwu1<|tSrg~a z-oI*eKWWI_i5)%%ny&u5vU|_(&B8|Kzw>WAZdI+K$68CiJy}>H=+9%>f$Ry9{i)Qs zb!u+$p*}+o`4r#c8hUKj{&eqg&{Xq-eFZ(#qQ3fxZ*-Y`-%ccX@pIfd(eXl>*Ce_zz_=0~k53)$yjG-Knc_nxHx{KU@UfkSr8iyCmfoAZ{wSGW$df}DQb$<%t| zF5xUQ)Ecv9#0A5>r%#Q(HKWlXVeJuy3vC20_copE--+0&ZbgN@|qzXr1Y2-cNIMvu=>l#ymJdce}NA@6)3q9KBbnM=jM!zo~OQxp}(R zH~j%A=N7QAWSEs*v3jBu#Fz3UQY9r%+}FiVMO7B|f8{;VnbdoYpsS~VZ{#QWU(xLU zO+83{>;Tn4e%Rw*gTL@0ng0>X6+pJ$U&s=fKlvkZ2)RNL-wVG}iU`yZ2YJVncm(nV zUP?3Z?!OW5pXgQ{=D%YBlD}e+KoA&!->%bEQquS@_|<<$^J>)p6$=m_a?qvQ#1Xh+ zKUT#5if)zZp~5g^SMo^iWyG+lGd3r-rR!{28%n~EJuqxpS@{3eE6F1tfyjgC7hv#z zM;`sJ+Ok@&Y710bpxOfeCoMqg=jwj;|4EBhWBoTQK>T#F*i+=iQ&QU8=l_m*kpG4q z{+BUH9(e@%2Vh92)Wlv%sXqGSn~8+G>D!* z_*2>cKf)3HxSk@h5ZCFBbf5k|iI?E{@O^lg5>y)PMBDj4!TV2Si7x(=y!c{d`j5et zmrD{(7#iSD;`%?KGr{xYOZf!Pap?a$^Fx0zbxcfC{a0;)Y710bpxOe}7O1vBwFRmz zP;G%~3shU6+5*)UsJ1}01*$DjZGmbFR9m3h0@W6%wm`K7sx44$focm>TcFwk)fT9> zK(z&`El_QNY710bpxOe}7O1vBwFRmzP;G%~3shU6+5*)UsJ1}01*$DjZGmbFR9m3h z0)K6R5atN9kIa!}b*&|0Pb0oiBIODMMgo!NByYZeXEZ|0^pLcW2BV! zl0f*Q&!VLDAm^MD407fix#G+xC4VA-zgD=CTYOWhBxK&TX5QhOQYG2%$m!x8(GzD# zQ96_K(dQddXADs~lXvuQd(gizMSjzbVCXY=TDT{_n?~LlFz*`TK8<-tpI57-q{kcz zcbq7Pzq;0e=4Q6Yph~iegd%n&`Vl)5yAnG_fYBS&2hK}|r!7n*^ZgUFejlGpedlKplP7%pajq+&}PsU zP$Fn6Xd9>}$Qon=vIX@5;S+2nD-Z`{1~LS71hoat0?h`^0Zl==#)Bq;xF9bODchth z7lK~|dI2K84NlIn?gr`(I)``XK^C}wiEA5N3vev~eFWVAWrK1+2B2M_-JlfE9?)LU zKG1g14p0)PE64>j0@MyP4>T9V2euz*5{T3jq+WQB_isVAxIcrdC9bbsfHFZ=;9bMDE@&W#)P>VQ5uh2MXiy9&7UT*V2^s|& z4RQmG0gVNX1C0l{gC>9`g18_LkSE9s!~=PQd_cY+KFAL=2^0sK2_p5R0CWKNigcNb zFe1=#ggXIB1IaM5aDN4K5%hwozuuBc5r2y6G2-v<5au3;=ty*}0V0w*AmT@QAYuni z5XrOQAd-K&AX0y*fJj}_5Y!Z;1yTpq1F3>)f|w@lw+oT21F8j51JwqRd?C-|z5$5T zX(VnE2f-rm$uo(sF^FI^0ui1zs0oPZF4K*KYYrk|TZ3AGT7p`DoI$cQ5=?^C24n{^ z1nGm?fqqlG+=~tf(;j31>IJd|k#cGcBIVW!)B|J*>JI7#>I$*|nS;zgrXUlLF~|tS z0d)a&26X~;1d;R+eTIRCf`)(wgPcH)AP3MOkUeN1XaJ}`s2`{=s1K+&$P45N@&Jtk z5q~CrO?;gAI`Mhp>s%1=ZJD1>#69upyC7n_Y|ssm9_T1&4`?N51&G*V8E7fU4zvJ7 z{GXJAa^@=`J}@6722BPDK$AeEtcXAXAb*gMc@D;P3Md2=2$F&%AX!=nKL`{G3Ik08 zO$E&XMS^C5rh{gJNSTWUk@w_X1apnTbp|K~6bp(25qTmzo4L-zbuMTjXbC7Dva4Eohf*?8tXd8&g?f@l$ zc7YCq4uKAWh&>K~#(?&N_JQ_-xS%7T>!53(EYMZZ70_kSCD27sCg=j_Jm?(gEa(g< z19Tdc4mt%&1Dyn&0HuPCgN}jjfJiz>x^97p{x?B6pmrdVzS|)3j@;*hh#$QL6@l)9 z3PEo`&p?ksPeCNFo`CW|k3bJW4?y`K66OWyIp`(m73ejn07UYa$VM|)@=W>w-*9yX zeFc#`DF%H8eE_`&eFA+1eF2fagRCF%6VEmAO!^|4xF`J*Qh8?M@&oi8M0|j}BVkFH z66Q+yzd)pKasyX_N%|EE#XJFucfDt2mFn2q_1_Y$C`B;A83i8O1IMfuc>^fQU_rhps1*)KXc3r3{+K9 zb~iQZYSdMJV$8MimsY;GbSGG?1_g|Nvh#ifsWm0}1G4oQ84HwN2{|_ur6x0TPI8_0 zU*s}P@U|}ES#J>0rE?6=L(Y+%qHJ@`Vy0>ThJHXZGcq9>=s^bhMdZ)h)!u&I-iR8? z?s#uxiW<+C%Lh|w!t|I7n~~4#z`=XMA!vkSsKVDASm#>^#W685K~2f?2=oDGew30$ zo=)aja4d|>Nj$s|o+lTaX>M+?F+#@ z!7(#JO!8BJ(?cE|yXf>Yu!b^Y0;-(XgN&|a)Q}uuw4R(pN+{v1w{N=5gKy(R#RDfb%9bNfy&Rd3a zF!{D+^i;hJIfwYXCS+U^G7l}DG)N@p5btjUPRsT>yQfX+`j$$mInj;uI~#+eIxMC*lppv>yhQ$pD=G5%!9_QHx@s$T!V~!;PEIU3QPU{U z`J&ZzIfsmGh`-fxJhwjiZqXF&Qj3z_7qQzS<1umV57_k3R(8i5qAeLWY2d1*uiI$E znjbIZGIPNpDV@CNw77j2pHO)`mFEH<<)tZPRyRKMN+;&{Bsv#}zeRx49GB2{2X5!~ zIv&n&%q)~%FftPddDd$?+x0Fu=I}SztpuDFkol^`oAk@#@=9<_jVz2zQRq}?yXF15 zyvxv{!3v6FM!dK3cq&6PQIha|_(E@y(sZ|8H7_rJyAWu^6`{d)#6xUd%XT&|Ue!>e zhB980qM)FZTmZLXPy9W%WgcIlZe586m6yiK{aPRj3?L;~E9BLtrl*_eGZuw)D$hBm zT68H%pFSi{^zfKFC4&+~kEY7{s7&kCjEt>&jpNxxY7ePgpzPMTHjU=6_fAjMj|W#V z7KH|t*JAsi@!yvE%56YuBuPN1((sXiar5%79%Rx3i&kPC#@0bxfl|Jklb-eI{yL1- z;CS(P0UX|B!TI9lZ}d&}9{>lrL;THJjn>-O&)zk1mikRfhN_hZfQVUhztrTFdr0NE?JuDeP};mdBj>=a1@@#CqpJAQ zbFhaAa_u#?^j-wrER4)Zrpiuc)NE(dH@NnPL*Sq&n8HN@L=vf|M4~i~dnNOYlUgJ= zmMEtdrb^xtUJy?xl^k9;qiIaxtOqK};bv%m;8g-11n{@Y`udB^1~uQxYZnQZ=D0lX zGe>KYZwQMc4HX1Js@FQMxw>sknw%r$i6!Jzw|6tflx%IM+#B^iJ!Anab-#LsX)q1cx6El$^KBF#{TmL#iYTa7bIybXk|s`#XQWlB3DajGr)6 zSHErg&AG4v(rMHkvk8zPwX$Tk{jdF7^rwOYDB?Ru8MMwho5a^9?HxyP=++Dcp&?`* zJUF>F$t<;p3hEswfO<5S;qZ@GZ?mZ7|4hy)U^vg0N#Aem_4$~bQ^IggJzcb|QPY9N za*h^~N#g0|bm`#6`2AsWPCIZKfn(BV)r3Q}G-}8>-55^7_He&KHG}JN&LD;}OC{NN zf|gZ?oHLH$sA<`DKCHatlAI#~M;q~+uRUz}fEWJha!wS(Y4+`#sY_-zo}9Cs;iM}S z4s8FjhMk;~#Bg$>ya$*ryL(E`ImvK3H8eC=o}0f>&bh&GJevHdqoJ$)Sj+F(sPWQqYn;xgb^;bS)E(9(%LHL zSTURiP5ZrEw=3wJoHLB!Bn{w?7x(J0M$YkOID&}@B{vrx=`H6>1&6f3*EVXl$}oI6 zRL=Rmz9xd-+qM>s{=8!h{n0Awf;}@))p`wiHfU4iT!D$K3fs!}x>*{10$T>5> z(FMnORbipshHJm%oQ>eL2B((vx|iU}2Y8hX?Ia_!&p>6o$%~jqa?S&WWBGQ<2I7EYIy$#ep_3f7-=S&1g z51j5Dc3PT!&l(`-M1Z3YPJCobxA34(hH}nk25rQHlN)D#jMR~H(!nA9onEz_y~VRG zc*!}r;Ism#RoGDFr&DtxJ$jlP2?r?wBpayb|NXV1& z{dr2Gtdb_gX(=y99nuY%ikhzp1W4cI=8yqfUoXhBplT$FqYDnHkqqBGD3UaNOKNha z@81y|R2C7#!Uf0g?b(+M4*K_`zP4aECtUcaYy7%F`bg-Bk$8H6gW?czQSD`Whas7l zz+rk%jtnPglv%5|sY5csAtM&Zi~Bx;LtN6MIt}N;fZXNWBx_dUf>kQR3;~ zpbt%CNIS=w#0$MHIr%Hc#kvkSsNpN&Fg?hga6{4t+ckc;!s?{jG{`Wz1^eM8v1?!!$GagmtbrJ&bBV2?hbGG>Xuwa9qvK2uKRLSt6m%CeFKNl&7bej_vCmB zcaQJauHN!BufSn+YXt`-V~kd}qQ-Q6=w*DA ziU+w08Ie#T5^$C7^_sT!Ys{%`6bCJ;!jT^H%Oz#oZ89*aU<}BND9I`L9-$aF-kvzU zm9Qim@tBf9pox;7C=^OyZlPXKw4V0YcuE7RKB&B%>kJ!^JRSP=^nv3l9;7!&d=$2} z1&8>~n=c;sCp1zcKFU}}_In&<`XQ!0putGU5bu5R=BQ4Z-p^|_lpPopNuZ~gE8!^} zJQGlF_29;0N;h-Tjh=`Qq%3q=wQJ$#Fu@dX(4v!$JRh89;9N3nkyqdlw*zf`cO!FR z>_|o?X59ISkxq5d*1JPv@>jmU0}ko$n3_?uw?sGDh+kXT*2V(EcN3*bMn=$}Eh;Li zoT~Q_Hf-!T2)Z%8Bm1oa?@@7uI&MSy6RlAgF{bSd87;&UmaD6I$&(XGrN`8$8|E3q zXdSP+WXHT49oQEfGkT03z!!7z+f7Q-3txv?J2Y9(#Dg?h0}WMa#4BgdDZ}O((ql7I zqaK(YLbXTggDsy!yoRfvZ9v6Cd9OM+q%641zPYRQXotFTv=)qvYO&XoSNXF|vqJyZqoRA3^=5Yhjrq>At~J!`b%WKp#^#l=*PnbCQ8ja(z)?dpqvuWw%=XIkoo{J z*5DACFW;v4rjK5-rG~Nr-jH5~6RyPLTeZz=msY3k5y+4h1G-HDhv??zb~=5X#s<|I z$|LaxOrMl-Rca{Dz#HP5_i-gTH^ewS zp?T(MZOAaCqynv(mZ<{Wn6^>B6Rq_aEMJ zQKbMQIz3v=&n$12Wl6@t1Pxjr1c&75mECQ7t{k!F6gU>ltfANfH5S&Fyz_3aJP=Ll zDpImw>(h{F2${{D9?T6K)X{;Gp}aRBP>2S|i`UWCV|I6S)$!nvp)2BX0}-zZY01yZ zo8|r<98!XTCOeyX_L`fcR7TdvxY5=IZVjp5YjRr~QcQYaRyTyINb3*MUNQew%@e5O zEii8hH0o$?@GQR7PaU;%k2^SKsQpn&Bm%C4^eGC~*3s_Bi@QQ;K-qxokrDGSo9}Y= z!fjrnA2?m9qOIi56Z`NW`!TWMni}!mq+dnNaiK-$i#enPkYt?6y|}c?4 z1KJPBka+5@&AS_2JKr1}VmELip?M>4F4rHNJ?21BM|!5DD{|SJE0IpdHR!R!4U-{p z#1qj16B~$mffCZrY3TPK(Q9cJdR$N0;P+f0)d4~4gMXwYoLRf$y1|?FZl%zuHh4Zb zqz~Iu*WNuW-ocULP<@v%C_N;lyY3x((?-&T_yJQVRxm1Ha;}0=0Z{>3S1>AIYRgL6 zIoPa%c8YP#~rmF>7|)zO&gha)Ww6E^`JNv&f-v<3g^Wr zPKEP36sN+OEQ(X%TpGpU3Pb(5QcvG6fnTFtU&mwiI2`Xui}8D%2o6;%|6ccoGbK2| zj;{IoX{2nG#%0X`8spc$*KgrW$x2~lrq&w2+GgGsA*BIT>cu>+7x~46wz|ukw`%Kx zc}r+O)nb**-9qaM>uczM-3Yzuaf#QwgCqI())k^gV#?|78c?-A>YWPFsM^xC58ZPx zQmwgaL}HWnKqI{k)H~(pqBYT?qawG&`Z;7sO+F|(=<~1{CrBI2^eF;_KGcuDZu?TZ z>C9%c2^wiJpg|~x+7i^k3tm6o(LJK@JCW&1H9(=JmO?O;`n^|v|MY}34IHM1#qZz+ z^1*4=A)1@L!E6{fOl|4S7kY89B&`07udZ{zSwhKBJrv!(v~Cx=m25Qf;*s`;w6xHm zBRHh?ukXG_?^&0Tq-|vERzaT+eGMYhm65qsFE(mNUV@ffrZ>Z}@yYU)y}$hOz6y5ibGix9L&5(l|GFhHt(k-vlBRI8HsfQI8retFq9%kb#9t?V7xcQ zp>w+<)7y}bKDyB|74(RiJgqzzD(I~{BL4s>)>s;uJ{dD%51EaS^@y2Y z+i5)L>Bt80`uX5=N9}?VJW0R{6eB&OY-Y?Gf5b79LZfE8U4g*d< z*Sri48BM{Wm`zs?G#E=_Kb_4J|&0LmZse>l;}$4`Mf%#G0g{EHt*xuhgu#@TA6_xL{7$Sx43MnY}} z3&HGl=GJ7pnw$i1n07(-J4YwKdmjxtu|A2?jVeE(_+*2RpJ;!-Jmsd&)HY=N!_-K> z&ppZNYr+W=@IBTPS{_q&eMQeOQNEJ{8;}v@>w@J=CKU@veIV1|_nA4S=lxe2R4MZ> zd0N5DHnYM}xsO)pZ&X`SiNDdMq|*8L%Dl>pCy|o;j`;lYoTt7a-DXia*LfJ--?=fT zsIWo9Z)9DD)O;wxm5;zIAw%-Vz5a=i%lBW9T?V8CL#8jov469F)3{e#?HE6RkCtEg zuA$tC@;~e_bf)Y0IwI?{ubfvMLvIkZB_@BAHZXhZCOTnT5dO-3Aw>VGno<7{EW``B zg5gpzU+812XDZ)*XDQ!)hq)C`3M1{i_7?Gyh1wo`p)q-<@nDm6VXMhUr4lUW<5$|o z-_VP6@pk+Y+J@FD+Oq2<<#*hj0>2S#kojS`P57zMqHH{qadC0?g^3*#o15Rq zGpYa1`b)b98AshMzfO>WnSNr!T-Pa^M#64?VUB!K5B zl|6|Ag+iWK_9PK<10=p8X|Qi72O|{{3x|XtSX}&2pIGDvS)|>c8xX*XAVADRrOxpd zi7|-s1uv9?t#LBcKtAc)qA}B(I#tishU`=^GGXk@PKbwC6b!Gj<9Yc|(UL_|;~;+p z){@0gCVm*N%(L8v3XoV9ggonsLv{a24k(bW&^W*;H57Cpl_*f`{KR~uFPJ9~82j*q z9GN9qucc%bPR39aYh^Md1#ArXAUcke8?mDo^H4Dc@i>8EKKr#q6e#v2Im{uGxSoHg zKshO>Q0@&3Nk$%37?PDFmU@&aOO@qNBlEvNV%Zr;tS8Dazu85?lXAFXF*o#2lP(KS z5-q!@{Dvy`3Q#I2XTYf7t=xa`xu@qO50C=b(lA6JS$L$CjLa3=pV?+f6<^&S0+o< zs}QBGa!Jy8;PE)YT)`xsSV1 zkt&{+VD2b`SLNwsYb+um7Y=FGDKmNc@;oPTcw(^#TN}`TN%pvR!HK_cD&F7k%m11MlpNGV&Oeh+vOen6 zD)ao8SZwD26#MZnkyx$=AlB1g;;`HrIIO3?#G&1YDl))fJ^d37RUAuksHcCzp-NUM z4)yexI5N*c)dC#W(_iAqyoSbMJ^dpN(gJ;CIC%O?9C^(O82Q6rqR6XkpvWKo5{32_ zQp*EH{_vM5@`V1fnKrb{>wf}fx59Fr*>C?0 z7CU($j{h9`b8yn&E2yu@+#U8q98`*01${VcbHru zd#e@4MlHrehPq;!iJ8#B8X%d==K7NvUHZ;bEW)gpB!C+%#1a7(k~zE(zLeT*&V;~% zC|NY*khwwMK!2`~LkEz&Q-NNO_qKx9KVVf4R>_=+8v?_U4EiTDfD61ziM%g!V6d41H{JanaYL^Me0;J^r$S-BC0AblJe>PfgvIE^9FRW|#|=dSYSrvw{eiV1SniU}m0KwP%Vq zPfY4<2?r$|i5CR=;{aj_hcCpgB5$&7fm)GLxa?U`aLVMrqhMr?qEO{J3H{Y9FqrRW zDetOsRVWMs{$VKeO!J6O061iuENT>y2ifPs!TJuGIOdaB4)*LRFdZutNeulNEs;U~ zkU|0%wYAYBMcYKrTF;_tBo+#_hqsD~nx#D?Sx<<+>c*)|f+1h#nQADiT8aG4D}eIb zJIYt(#g&IGSMbg%mI}!Sb^s$64!*V}-)h5{e%Jyb3E*)jk$sWWryErLDe(*xi$k#y ziRU90q1eJ7`2w#11mk)H%Bv!sx6NR^-<4=NUb78Ns)uBxlS@=5`%{3}$#Rp%8e zos>f8R?w-27G$?Mu@1dylnK6x;s?x;`f1mZvry05;9>Y36oz%RY0ZC_PcB)LJp z1&5^U7CnWkw_}v%A|puUD2H^DsobVM!a-#jz(uc9Bvg=d(tG4LS-?hy+3_V@uBXJ9`ewy0U=qHAfv;gB zJ^AiMfokQ00=-m>)SgxM@XEvtaY_TKA3-IFbi>iP3+8)CebMK&Vn5qq*D^LJvR`qq z@JsjHNlwWG0?=GY_;!>pn#$QEaIlz5b4l3J2Rr(E(9&lTQh^mp!$JSjd#Z;@RlFo} z3{FCEpp^m;xn4k#KTus9%3mm$s=9>oKtPc{P(zHW>QEA?%+`z!vPaaxWXk|Nh37Bw zITVs{1BBtAjR@g0Z~2Jmn#kS z!PZ+veM)&CfXN@^&L9wZ;gcpKiAXH<2o2_W@Vx{~DIu+-qRQ^~z$A*_5Rm4ow}VM> zMg*kEg=z|`vaalW2*}88Q8r1}SaF_~NiBrSJVVo}r@7eUGyt$4|7Z@Vf}tNtgorv@ z5nn)-4hZFqhJY`i$A}7?wKNP#lq@`I-m1Gz^ifX`qi-mgs?10_(jH_QioW4ceMYKo zA;%?9$1hO3pE-d-#a%5{D4?;PkegN4bfu9&w)7qez)p4I2XeL6t~l0|Kq=3wbvM{Q+Sh_BJZoz+4&zIHmXU zZkGa&r)Dw9{s{U8)drc2R9yU7ISxVA6O^vHq(DC+LTgG1ld6r%0B;dihMJu5`byY)2-wOspW$ zr2?fJPM}WdJq!8IF_;Z`ti29${r((Esc>ilO1)J3W8mq!>uYZjq^GDih|Kg3MlBM|ul)v;v*7j(iu%7-AN0urX z(!ZojzEch=$shhwBlf-*InKZ6gB~IE{R&-TSxxkS06x4FIxRB#SJUDjrFd-3~yaFS9>DK9fXE2;tNWYL9>dTbBt3yfV+k;h04* zEacPYQYgTp0|11+!SJVagM{BCbR@Y77gDHW)*wt>IXGdB8J6Q50L*<8e~+C_kA4{UFNR zVNl0RLDJ{aQPm_hv?j}DQbiRED-`fpPk%HZXGI5T)|0$iP>=(%WAsSG%!zvPlqi-X zhwRZ}iJ&v5GAXQ-=`g^jZ!p1%1J0^wqnPvlh&iy`1tBc7h}1WVci=qN){2oE8ZY{D)0t+jg+;gF|g@ z48U$WvI7l=9f)uMhoTC`G*sNbqtASV5Pid;4}7db zh&ogdGSn3bW4e*Dt*2G(apR-c0BIj`rHoD$8EWseHxIj$a0H8#AEd|+a6%yf@Smz& zsRq|uf;n`0Kb8W!vO)oX^#u2?dKpg^0YqiDRFSP}`z3P#A;g%wKWbe`6yy*@CN^?Q zX<1calc~nmk~#;M`3&{kdUEosOqGuVn~jy zgao<3SyA;>)perIULEdtF0Wiry+sloKwXKBqz5*m|Wm>%)S_^ zp5{yWitR~`c7-Up5Pv7JDUJ*0$09ub|51ihCpc27Va!7rzN$8;qShJ!vWA;#$0&P{ zkqrkMf&97nw1=!{V2>7Bj382Rqr#S=-d0fBSfPN Jj{k%I{y)}|6N3N% literal 0 HcmV?d00001 diff --git a/packages/otp/package.json b/packages/otp/package.json new file mode 100644 index 0000000..b16e3b4 --- /dev/null +++ b/packages/otp/package.json @@ -0,0 +1,33 @@ +{ + "name": "@prsm/otp", + "version": "1.0.0", + "description": "", + "main": "./dist/index.js", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "test": "vitest", + "release": "bumpp package.json && npm publish --access public" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "hi-base32": "^0.5.1" + }, + "devDependencies": { + "@types/qrcode": "^1.5.5", + "bumpp": "^9.5.2", + "qrcode": "^1.5.4", + "tsup": "^8.2.4", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } +} diff --git a/packages/otp/src/code.ts b/packages/otp/src/code.ts new file mode 100644 index 0000000..b6b11c4 --- /dev/null +++ b/packages/otp/src/code.ts @@ -0,0 +1,34 @@ +import QRCode from "qrcode"; +import Otp from "."; + +// Step 1: Create a secret +const secret = Otp.createSecret(); +console.log("Secret:", secret); + +// Step 2: Generate the TOTP URI +const issuer = "app.example.com"; +const accountName = "john.doe@example.org"; +const uri = Otp.createTotpKeyUriForQrCode(issuer, accountName, secret); +console.log("TOTP URI:", uri); + +// Step 3: Generate the QR code +QRCode.toDataURL(uri, (err, url) => { + if (err) { + console.error("Error generating QR code:", err); + return; + } + console.log("QR Code URL:", url); + + // If you are running this in a Node.js environment, you can save the QR code as an image file + // Uncomment the following lines to save the QR code as a PNG file + // QRCode.toFile('totp-qrcode.png', uri, (err) => { + // if (err) throw err; + // console.log('QR code saved as totp-qrcode.png'); + // }); + + // If you are running this in a browser environment, you can display the QR code in an element + // document.getElementById('qrcode').src = url; +}); + + +// Store the secret. You need to validate the secret against the TOTP generated by the user. diff --git a/packages/otp/src/index.test.ts b/packages/otp/src/index.test.ts new file mode 100644 index 0000000..8133229 --- /dev/null +++ b/packages/otp/src/index.test.ts @@ -0,0 +1,246 @@ +import * as base32 from "hi-base32"; +import { describe, expect, it } from "vitest"; +import Otp, { InvalidHashFunctionError, InvalidOtpLengthError, InvalidSecretError } from "./index"; + +describe("Otp", () => { + it("should generate a secret of default strength", () => { + const secret = Otp.createSecret(); + expect(secret).toBeDefined(); + expect(secret.length).toBe(32); + }); + + it("should generate a secret of low strength", () => { + const secret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_LOW); + expect(secret).toBeDefined(); + expect(secret.length).toBe(16); + }); + + it("should generate a secret of moderate strength", () => { + const secret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_MODERATE); + expect(secret).toBeDefined(); + expect(secret.length).toBe(26); + }); + + it("should generate a secret of high strength", () => { + const secret = Otp.createSecret(Otp.SHARED_SECRET_STRENGTH_HIGH); + expect(secret).toBeDefined(); + expect(secret.length).toBe(32); + }); + + it("should generate a valid TOTP", () => { + const secret = Otp.createSecret(); + const totp = Otp.generateTotp(secret); + expect(totp).toBeDefined(); + expect(totp.length).toBe(Otp.OTP_LENGTH_DEFAULT); + }); + + it("should verify a valid TOTP", () => { + const secret = Otp.createSecret(); + const totp = Otp.generateTotp(secret); + const isValid = Otp.verifyTotp(secret, totp); + expect(isValid).toBe(true); + }); + + it("should not verify an invalid TOTP", () => { + const secret = Otp.createSecret(); + const isValid = Otp.verifyTotp(secret, "123456"); + expect(isValid).toBe(false); + }); + + it("should generate a valid TOTP with custom length", () => { + const secret = Otp.createSecret(); + const otpLength = 8; + const totp = Otp.generateTotp(secret, undefined, otpLength); + expect(totp).toBeDefined(); + expect(totp.length).toBe(otpLength); + }); + + it("should verify a valid TOTP with custom length", () => { + const secret = Otp.createSecret(); + const otpLength = 8; + const totp = Otp.generateTotp(secret, undefined, otpLength); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, otpLength); + expect(isValid).toBe(true); + }); + + it("should generate a valid TOTP URI for QR code", () => { + const secret = Otp.createSecret(); + const uri = Otp.createTotpKeyUriForQrCode("app.example.com", "john.doe@example.org", secret); + expect(uri).toBeDefined(); + expect(uri).toContain("otpauth://totp/app.example.com:john.doe%40example.org"); + expect(uri).toContain(`secret=${secret}`); + expect(uri).toContain("issuer=app.example.com"); + }); + + it("should throw an error for invalid secret length", () => { + expect(() => { + Otp.generateTotp("shortsecret"); + }).toThrow(InvalidSecretError); + }); + + it("should throw an error for invalid OTP length", () => { + const secret = Otp.createSecret(); + expect(() => { + Otp.generateTotp(secret, undefined, 5); + }).toThrow(InvalidOtpLengthError); + expect(() => { + Otp.generateTotp(secret, undefined, 9); + }).toThrow(InvalidOtpLengthError); + }); + + it("should throw an error for invalid hash function", () => { + const secret = Otp.createSecret(); + expect(() => { + Otp.generateTotp(secret, undefined, undefined, undefined, undefined, 999); + }).toThrow(InvalidHashFunctionError); + }); + + it("should generate and verify TOTP with custom time and interval", () => { + const secret = Otp.createSecret(); + const customTime = Math.floor(Date.now() / 1000) - 100; + const customInterval = 60; + const totp = Otp.generateTotp(secret, customTime, undefined, customInterval); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, customTime, undefined, customInterval); + expect(isValid).toBe(true); + }); + + it("should handle edge case for time steps", () => { + const secret = Otp.createSecret(); + const currentTime = Math.floor(Date.now() / 1000); + const interval = Otp.INTERVAL_LENGTH_DEFAULT; + const boundaryTime = currentTime - (currentTime % interval); + const totp = Otp.generateTotp(secret, boundaryTime); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, boundaryTime); + expect(isValid).toBe(true); + }); + + it("should not verify an expired TOTP", () => { + const secret = Otp.createSecret(); + const pastTime = Math.floor(Date.now() / 1000) - 300; // 5 minutes ago + const totp = Otp.generateTotp(secret, pastTime); + const isValid = Otp.verifyTotp(secret, totp); + expect(isValid).toBe(false); + }); + + it("should not verify a future TOTP", () => { + const secret = Otp.createSecret(); + const futureTime = Math.floor(Date.now() / 1000) + 300; // 5 minutes in the future + const totp = Otp.generateTotp(secret, futureTime); + const isValid = Otp.verifyTotp(secret, totp); + expect(isValid).toBe(false); + }); + + it("should verify RFC 6238 test vectors for SHA-1", () => { + const rfc6238TestKeySha1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // Base32 encoding of '12345678901234567890' + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 3, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 2, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 2, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 2, 2, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 2, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 0, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 3, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "94287082", 0, 2, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + }); + + it("should verify RFC 6238 test vectors for SHA-256", () => { + const rfc6238TestKeySha256 = base32.encode("12345678901234567890123456789012"); // 12345678901234567890123456789012 + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 3, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 2, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 2, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 2, 2, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 2, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 0, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 3, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha256, "46119246", 0, 2, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_256)).toBe(false); + }); + + it("should verify RFC 6238 test vectors for SHA-512", () => { + const rfc6238TestKeySha512 = base32.encode("1234567890123456789012345678901234567890123456789012345678901234"); // 1234567890123456789012345678901234567890123456789012345678901234 + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 3, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 2, 0, 59 + 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 2, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 0, 59 + 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 2, 2, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 0, 59, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 2, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 0, 59 - 60, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 3, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha512, "90693936", 0, 2, 59 - 90, 8, 30, 0, Otp.HASH_FUNCTION_SHA_512)).toBe(false); + }); +}); + +describe("Otp - 6 Character Length", () => { + it("should generate a valid 6-character TOTP", () => { + const secret = Otp.createSecret(); + const totp = Otp.generateTotp(secret, undefined, 6); + expect(totp).toBeDefined(); + expect(totp.length).toBe(6); + }); + + it("should verify a valid 6-character TOTP", () => { + const secret = Otp.createSecret(); + const totp = Otp.generateTotp(secret, undefined, 6); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, 6); + expect(isValid).toBe(true); + }); + + it("should not verify an invalid 6-character TOTP", () => { + const secret = Otp.createSecret(); + const isValid = Otp.verifyTotp(secret, "123456", undefined, undefined, undefined, 6); + expect(isValid).toBe(false); + }); + + it("should generate and verify 6-character TOTP with custom time and interval", () => { + const secret = Otp.createSecret(); + const customTime = Math.floor(Date.now() / 1000) - 100; + const customInterval = 60; + const totp = Otp.generateTotp(secret, customTime, 6, customInterval); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, customTime, 6, customInterval); + expect(isValid).toBe(true); + }); + + it("should handle edge case for 6-character OTP time steps", () => { + const secret = Otp.createSecret(); + const currentTime = Math.floor(Date.now() / 1000); + const interval = Otp.INTERVAL_LENGTH_DEFAULT; + const boundaryTime = currentTime - (currentTime % interval); + const totp = Otp.generateTotp(secret, boundaryTime, 6); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, boundaryTime, 6); + expect(isValid).toBe(true); + }); + + it("should not verify an expired 6-character TOTP", () => { + const secret = Otp.createSecret(); + const pastTime = Math.floor(Date.now() / 1000) - 300; // 5 minutes ago + const totp = Otp.generateTotp(secret, pastTime, 6); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, 6); + expect(isValid).toBe(false); + }); + + it("should not verify a future 6-character TOTP", () => { + const secret = Otp.createSecret(); + const futureTime = Math.floor(Date.now() / 1000) + 300; // 5 minutes in the future + const totp = Otp.generateTotp(secret, futureTime, 6); + const isValid = Otp.verifyTotp(secret, totp, undefined, undefined, undefined, 6); + expect(isValid).toBe(false); + }); + + it("should verify RFC 6238 test vectors for SHA-1 with 6-character OTP", () => { + // const rfc6238TestKeySha1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // Base32 encoding of '12345678901234567890' + const rfc6238TestKeySha1 = base32.encode("12345678901234567890"); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 3, 0, 59 + 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 2, 0, 59 + 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 2, 0, 59 + 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 0, 59 + 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 2, 2, 59, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 0, 59, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 2, 59 - 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 0, 59 - 60, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 3, 59 - 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(true); + expect(Otp.verifyTotp(rfc6238TestKeySha1, "287082", 0, 2, 59 - 90, 6, 30, 0, Otp.HASH_FUNCTION_SHA_1)).toBe(false); + }); +}); diff --git a/packages/otp/src/index.ts b/packages/otp/src/index.ts new file mode 100644 index 0000000..70f3c7f --- /dev/null +++ b/packages/otp/src/index.ts @@ -0,0 +1,186 @@ +import * as base32 from "hi-base32"; +import * as crypto from "node:crypto"; + +export class InvalidOtpLengthError extends Error {} +export class InvalidSecretError extends Error {} +export class InvalidHashFunctionError extends Error {} +export class InvalidSecretStrengthError extends Error {} + +class Otp { + static OTP_LENGTH_MIN = 6; + static OTP_LENGTH_MAX = 8; + static OTP_LENGTH_DEFAULT = 6; + static INTERVAL_LENGTH_DEFAULT = 30; + static EPOCH_DEFAULT = 0; + static HASH_FUNCTION_SHA_1 = 1; + static HASH_FUNCTION_SHA_256 = 2; + static HASH_FUNCTION_SHA_512 = 3; + static HASH_FUNCTION_DEFAULT = Otp.HASH_FUNCTION_SHA_1; + static SHARED_SECRET_STRENGTH_LOW = 1; + static SHARED_SECRET_STRENGTH_MODERATE = 2; + static SHARED_SECRET_STRENGTH_HIGH = 3; + + /** + * Generates a shared secret using a specified strength. + * + * @param {number} strength - The strength of the shared secret, defaulting to Otp.SHARED_SECRET_STRENGTH_HIGH. + * This determines the number of bits used to generate the secret. + * @returns {string} - A base32 encoded string representing the generated shared secret. + * @throws {Error} - If the strength parameter is invalid or if there is an issue generating random bytes. + */ + static createSecret(strength: number = Otp.SHARED_SECRET_STRENGTH_HIGH): string { + const bits = this.determineBitsForSharedSecretStrength(strength); + const bytes = Math.ceil(bits / 8); + const buffer = crypto.randomBytes(bytes); + return base32.encode(buffer).replace(/=+$/, ""); + } + + /** + * Generates a TOTP (Time-based One-Time Password) Key URI for use in QR code generation. + * This URI can be scanned by authenticator apps like Google Authenticator or Authy. + * + * @param {string} issuer - The name of the service or organization issuing the OTP. + * @param {string} accountName - The account name or email address associated with the OTP. + * @param {string} secret - The shared secret key used for generating the OTP. + * @returns {string} - A URI formatted according to the otpauth URI scheme. + * @throws {Error} - Throws an error if any of the parameters are invalid or missing. + */ + static createTotpKeyUriForQrCode(issuer: string, accountName: string, secret: string): string { + return `otpauth://totp/${issuer}:${encodeURIComponent(accountName)}?secret=${secret}&issuer=${issuer}`; + } + + /** + * Generates a Time-based One-Time Password (TOTP) using the provided secret and parameters. + * + * @param {string} secret - The shared secret key used for generating the TOTP. Must be at least 16 characters long. + * @param {number} [t=Math.floor(Date.now() / 1000)] - The current Unix time in seconds. Defaults to the current time. + * @param {number} [otpLength=Otp.OTP_LENGTH_DEFAULT] - The desired length of the OTP. Must be between Otp.OTP_LENGTH_MIN and Otp.OTP_LENGTH_MAX. + * @param {number} [t_x=Otp.INTERVAL_LENGTH_DEFAULT] - The time step in seconds. Defaults to Otp.INTERVAL_LENGTH_DEFAULT. + * @param {number} [t_0=Otp.EPOCH_DEFAULT] - The Unix time to start counting time steps. Defaults to Otp.EPOCH_DEFAULT. + * @param {number} [hashFunction=Otp.HASH_FUNCTION_DEFAULT] - The hash function to use (e.g., Otp.HASH_FUNCTION_SHA_1, Otp.HASH_FUNCTION_SHA_256, Otp.HASH_FUNCTION_SHA_512). Defaults to Otp.HASH_FUNCTION_DEFAULT. + * @returns {string} - The generated TOTP as a string of digits, padded to the specified length. + * @throws {InvalidOtpLengthError} - If the specified OTP length is not within the valid range. + * @throws {InvalidSecretError} - If the provided secret is less than 16 characters long. + * @throws {InvalidHashFunctionError} - If the specified hash function is not supported. + */ + static generateTotp( + secret: string, + t: number = Math.floor(Date.now() / 1000), + otpLength: number = Otp.OTP_LENGTH_DEFAULT, + t_x: number = Otp.INTERVAL_LENGTH_DEFAULT, + t_0: number = Otp.EPOCH_DEFAULT, + hashFunction: number = Otp.HASH_FUNCTION_DEFAULT, + ): string { + if (otpLength < Otp.OTP_LENGTH_MIN || otpLength > Otp.OTP_LENGTH_MAX) { + throw new InvalidOtpLengthError(); + } + + secret = secret ? secret : ""; + t = t ? t : Math.floor(Date.now() / 1000); + t_x = t_x ? t_x : Otp.INTERVAL_LENGTH_DEFAULT; + t_0 = t_0 ? t_0 : Otp.EPOCH_DEFAULT; + + const c_t = Math.max(0, Math.floor((t - t_0) / t_x)); // Ensure c_t is non-negative + + secret = secret.replace(/[^A-Za-z2-7]/g, "").toUpperCase(); + + if (secret.length < 16) { + throw new InvalidSecretError(); + } + + const k = base32.decode.asBytes(secret); + + const counter64BitBigEndian = Buffer.alloc(8); + counter64BitBigEndian.writeUInt32BE(Math.floor(c_t / Math.pow(2, 32)), 0); + counter64BitBigEndian.writeUInt32BE(c_t % Math.pow(2, 32), 4); + + let hashFunctionNameForHmac: string; + switch (hashFunction) { + case Otp.HASH_FUNCTION_SHA_1: + hashFunctionNameForHmac = "sha1"; + break; + case Otp.HASH_FUNCTION_SHA_256: + hashFunctionNameForHmac = "sha256"; + break; + case Otp.HASH_FUNCTION_SHA_512: + hashFunctionNameForHmac = "sha512"; + break; + default: + throw new InvalidHashFunctionError(); + } + + const hmac = crypto.createHmac(hashFunctionNameForHmac, Buffer.from(k)); + hmac.update(counter64BitBigEndian); + const mac = hmac.digest(); + + const offset = mac[mac.length - 1] & 0x0f; + const macSubstring4Bytes = mac.slice(offset, offset + 4); + + const integer32Bit = macSubstring4Bytes.readUInt32BE(0) & 0x7fffffff; + + const hotp = integer32Bit % Math.pow(10, otpLength); + + return hotp.toString().padStart(otpLength, "0"); + } + + /** + * Verifies a Time-based One-Time Password (TOTP) against a given secret. + * + * @param {string} secret - The shared secret key used to generate the TOTP. + * @param {string} otpValue - The TOTP value to be verified. + * @param {number} [lookBehindSteps=2] - The number of time steps to look behind for a valid TOTP. + * @param {number} [lookAheadSteps=2] - The number of time steps to look ahead for a valid TOTP. + * @param {number} [t=Math.floor(Date.now() / 1000)] - The current Unix time in seconds. + * @param {number} [otpLength=Otp.OTP_LENGTH_DEFAULT] - The expected length of the TOTP. + * @param {number} [t_x=Otp.INTERVAL_LENGTH_DEFAULT] - The time step interval in seconds. + * @param {number} [t_0=Otp.EPOCH_DEFAULT] - The Unix epoch to start counting time steps from. + * @param {number} [hashFunction=Otp.HASH_FUNCTION_DEFAULT] - The hash function to use for generating the TOTP. + * @returns {boolean} - Returns true if the TOTP is valid, false otherwise. + * @throws {Error} - Throws an error if the OTP value length is not within the valid range. + */ + static verifyTotp( + secret: string, + otpValue: string, + lookBehindSteps: number = 2, + lookAheadSteps: number = 2, + t: number = Math.floor(Date.now() / 1000), + otpLength: number = Otp.OTP_LENGTH_DEFAULT, + t_x: number = Otp.INTERVAL_LENGTH_DEFAULT, + t_0: number = Otp.EPOCH_DEFAULT, + hashFunction: number = Otp.HASH_FUNCTION_DEFAULT, + ): boolean { + otpValue = otpValue.replace(/[^0-9]/g, ""); + + if (otpValue.length < Otp.OTP_LENGTH_MIN || otpValue.length > Otp.OTP_LENGTH_MAX) { + return false; + } + + if (otpValue.length !== otpLength) { + return false; + } + + for (let s = -lookBehindSteps; s <= lookAheadSteps; s++) { + const expectedOtpValue = this.generateTotp(secret, t + t_x * s, otpLength, t_x, t_0, hashFunction); + if (crypto.timingSafeEqual(Buffer.from(expectedOtpValue), Buffer.from(otpValue))) { + return true; + } + } + + return false; + } + + private static determineBitsForSharedSecretStrength(strength: number): number { + switch (strength) { + case 1: + return 80; + case 2: + return 128; + case 3: + return 160; + default: + throw new InvalidSecretStrengthError(); + } + } +} + +export default Otp; diff --git a/packages/otp/src/validate.ts b/packages/otp/src/validate.ts new file mode 100644 index 0000000..2392202 --- /dev/null +++ b/packages/otp/src/validate.ts @@ -0,0 +1,8 @@ +import Otp from "."; + +const totp = "355435"; // Replace with the TOTP generated by your authenticator app +// Loose validation +// const isValid = Otp.verifyTotp("IYKZRIYTTWVKXBDNG3VSY3FTQFEO3MWY", totp); +// Strict validation +const isValid = Otp.verifyTotp("IYKZRIYTTWVKXBDNG3VSY3FTQFEO3MWY", totp, 0, 0); +console.log("Is TOTP valid?", isValid); diff --git a/packages/otp/tsconfig.json b/packages/otp/tsconfig.json new file mode 100644 index 0000000..7fc5833 --- /dev/null +++ b/packages/otp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "es2022", + "target": "esnext", + "outDir": "dist", + "esModuleInterop": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "dist" + } +} diff --git a/packages/otp/tsup.config.ts b/packages/otp/tsup.config.ts new file mode 100644 index 0000000..cabe3b6 --- /dev/null +++ b/packages/otp/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + clean: true, + minify: true, + sourcemap: "inline", + target: "esnext", +});