diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..192b338 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + overrides: [ + { + env: { + node: true, + }, + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + plugins: ["@typescript-eslint"], + rules: {}, +}; diff --git a/.gitignore b/.gitignore index ddff63b..508642e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ node_modules .env -users.json -migration-log.json -bun.lockb +.settings +users.* package-lock.json yarn.lock pnpm-lock.yaml +logs +testing/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..999a527 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +/logs/** +/samples/** +**/*.json +**/*.csv + + diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..f651c0e --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,12 @@ +module.exports = { + prettier: { + trailingComma: "es5", + tabWidth: 2, + semi: false, + singleQuote: true, + printWidth: 80, + semi: true, + bracketSpacing: true, + arrowParans: "always", + }, +}; diff --git a/LICENSE.Apache-2.0.md b/LICENSE.Apache-2.0.md index 559cd29..db2b9a9 100644 --- a/LICENSE.Apache-2.0.md +++ b/LICENSE.Apache-2.0.md @@ -2,180 +2,180 @@ 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: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) 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 - - (d) 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. +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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 "[]" @@ -186,16 +186,16 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 Clerk Inc +Copyright 2023 Clerk Inc - 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 +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. +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 index 57111ed..97c7b08 100644 --- a/README.md +++ b/README.md @@ -16,47 +16,19 @@ cd migration-script npm install ``` -### Users.json file -Create a `users.json` file. This file should be populated with all the users that need to be imported. The users should pass this schema: +## Users file +The script is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the handler for your source. Please see below for more information on that. -```ts -[ - { - "userId": "string", - "email": "email", - "firstName": "string (optional)", - "lastName": "string (optional)", - "password": "string (optional)", - "passwordHasher": "argon2 | argon | bcrypt | md5 | pbkdf2_sha256 | pbkdf2_sha256_django | pbkdf2_sha1 | scrypt_firebase", - } -] -``` +The script will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object. -The only required fields are `userId` and `email`. First and last names can be added if available. Clerk will also accept hashed password values along with the hashing algorithm used (the default is `bcrypt`). +The only required fields are `userId` and `email`. -Here are a couple examples. +### Samples -```json -[ - { - "userId": "1", - "email": "dev@clerk.com", - "firstName": "Dev", - "lastName": "Agrawal" - }, - { - "userId": "2", - "email": "john@blurp.com", - "password": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy", - "passwordHasher": "bcrypt" // default value - } -] -``` +The samples/ folder contains some samples, including issues that will produce errors when running the import. -The samples/ folder contains some samples, including issues that will produce errors when running the import. - -### Secret Key +## Secret Key Create a `.env` file in the root of the folder and add your `CLERK_SECRET_KEY` to it. You can find your secret key in the [Clerk dashboard](https://dashboard.clerk.dev/). @@ -78,12 +50,12 @@ The script can be run on the same data multiple times, Clerk automatically uses The script can be configured through the following environment variables: -| Variable | Description | Default | -| -------- | ----------- | ------- | -| `CLERK_SECRET_KEY` | Your Clerk secret key | `undefined` | -| `DELAY_MS` | Delay between requests to respect rate limits | `1000` | -| `RETRY_DELAY_MS` | Delay when the rate limit is hit | `10000` | -| `OFFSET` | Offset to start migration (number of users to skip) | `0` | +| Variable | Description | Default | +| ------------------ | --------------------------------------------------- | ----------- | +| `CLERK_SECRET_KEY` | Your Clerk secret key | `undefined` | +| `DELAY_MS` | Delay between requests to respect rate limits | `1000` | +| `RETRY_DELAY_MS` | Delay when the rate limit is hit | `10000` | +| `OFFSET` | Offset to start migration (number of users to skip) | `0` | ## Handling the Foreign Key constraint @@ -93,7 +65,7 @@ If you were using a database, you will have data tied to your previous auth syst Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the `externalId` (the previous id for your user) when it exists, or the `userId` from Clerk. This will result in your imported users returning their `externalId` while newer users will return the Clerk `userId`. -In your Dashboard, go to Sessions -> Edit. Add the following: +In your Dashboard, go to Sessions -> Edit. Add the following: ```json { @@ -102,12 +74,14 @@ In your Dashboard, go to Sessions -> Edit. Add the following: ``` You can now access this value using the following: -```ts + +```ts const { sessionClaims } = auth(); -console.log(sessionClaims.userId) +console.log(sessionClaims.userId); ``` -You can add the following for typescript: +You can add the following for typescript: + ```js // types/global.d.ts @@ -125,4 +99,3 @@ declare global { You could continue to generate unique ids for the database as done previously, and then store those in `externalId`. This way all users would have an `externalId` that would be used for DB interactions. You could add a column in your user table inside of your database called `ClerkId`. Use that column to store the userId from Clerk directly into your database. - diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..6e7dfe6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,638 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "clerk-user-migration", + "dependencies": { + "@clack/prompts": "^0.7.0", + "@clerk/backend": "^0.38.3", + "@clerk/types": "^3.62.1", + "bun": "^1.0.12", + "csv-parser": "^3.0.0", + "dotenv": "^16.3.1", + "mime-types": "^2.1.35", + "picocolors": "^1.0.0", + "zod": "^3.22.4", + }, + "devDependencies": { + "@types/mime-types": "^2.1.4", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "vitest": "^1.3.1", + }, + }, + }, + "packages": { + "@clack/core": ["@clack/core@0.3.5", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ=="], + + "@clack/prompts": ["@clack/prompts@0.7.0", "", { "dependencies": { "@clack/core": "^0.3.3", "is-unicode-supported": "*", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA=="], + + "@clerk/backend": ["@clerk/backend@0.38.15", "", { "dependencies": { "@clerk/shared": "1.4.2", "@clerk/types": "3.65.5", "@peculiar/webcrypto": "1.4.1", "@types/node": "16.18.6", "cookie": "0.5.0", "deepmerge": "4.2.2", "node-fetch-native": "1.0.1", "snakecase-keys": "5.4.4", "tslib": "2.4.1" } }, "sha512-zmd0jPyb1iALlmyzyRbgujQXrGqw8sf+VpFjm5GkndpBeq5+9+oH7QgMaFEmWi9oxvTd2sZ+EN+QT4+OXPUnGA=="], + + "@clerk/shared": ["@clerk/shared@1.4.2", "", { "dependencies": { "glob-to-regexp": "0.4.1", "js-cookie": "3.0.1", "swr": "2.2.0" }, "peerDependencies": { "react": ">=16" }, "optionalPeers": ["react"] }, "sha512-R+OkzCtnNU7sn/F6dBfdY5lKs84TN785VZdBBefmyr7zsXcFEqbCcfQzyvgtIS28Ln5SifFEBoAyYR334IXO8w=="], + + "@clerk/types": ["@clerk/types@3.65.5", "", { "dependencies": { "csstype": "3.1.1" } }, "sha512-RGO8v2a52Ybo1jwVj42UWT8VKyxAk/qOxrkA3VNIYBNEajPSmZNa9r9MTgqSgZRyz1XTlQHdVb7UK7q78yAGfA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27rypIapNkYboOSylkf1tD9UW9Ado2I+P1NBL46Qz29KmOjTL6WuJ7mHDC5O66CYxlOkF5r93NPDAC3lFHYBXw=="], + + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-I82xGzPkBxzBKgbl8DsA0RfMQCWTWjNmLjIEkW1ECiv3qK02kHGQ5FGUr/29L/SuvnGsULW4tBTRNZiMzL37nA=="], + + "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqtr+pTsHqusYpG2OZc6s+AmpWDB/FmBvstrK0y5zkti4OqnCuu7Ev2xNjS7uyb47NrAFF40pWqkpaio5XEd7w=="], + + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-YaQEAYjBanoOOtpqk/c5GGcfZIyxIIkQ2m1TbHjedRmJNwxzWBhGinSARFkrRIc3F8pRIGAopXKvJ/2rjN1LzQ=="], + + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FR+iJt17rfFgYgpxL3M67AUwujOgjw52ZJzB9vElI5jQXNjTyOKf8eH4meSk4vjlYF3h/AjKYd6pmN0OIUlVKQ=="], + + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-egfngj0dfJ868cf30E7B+ye9KUWSebYxOG4l9YP5eWeMXCtenpenx0zdKtAn9qxJgEJym5AN6trtlk+J6x8Lig=="], + + "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-jRmnX18ak8WzqLrex3siw0PoVKyIeI5AiCv4wJLgSs7VKfOqrPycfHIWfIX2jdn7ngqbHFPzI09VBKANZ4Pckg=="], + + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-YeXcJ9K6vJAt1zSkeA21J6pTe7PgDMLTHKGI3nQBiMYnYf7Ob3K+b/ChSCznrJG7No5PCPiQPg4zTgA+BOTmSA=="], + + "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-7FjVnxnRTp/AgWqSQRT/Vt9TYmvnZ+4M+d9QOKh/Lf++wIFXFGSeAgD6bV1X/yr2UPVmZDk+xdhr2XkU7l2v3w=="], + + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-Sr1KwUcbB0SEpnSPO22tNJppku2khjFluEst+mTGhxHzAGQTQncNeJxDnt3F15n+p9Q+mlcorxehd68n1siikQ=="], + + "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-PFUa7JL4lGoyyppeS4zqfuoXXih+gSE0XxhDMrCPVEUev0yhGNd/tbWBvcdpYnUth80owENoGjc8s5Knopv9wA=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="], + + "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], + + "@peculiar/webcrypto": ["@peculiar/webcrypto@1.4.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.0", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.3.2", "tslib": "^2.4.1", "webcrypto-core": "^1.7.4" } }, "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw=="], + + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], + + "@types/node": ["@types/node@16.18.6", "", {}, "sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.18.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@7.18.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.18.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@7.18.0", "", {}, "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" } }, "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@7.18.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" } }, "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vitest/expect": ["@vitest/expect@1.6.1", "", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="], + + "@vitest/runner": ["@vitest/runner@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="], + + "@vitest/snapshot": ["@vitest/snapshot@1.6.1", "", { "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" } }, "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ=="], + + "@vitest/spy": ["@vitest/spy@1.6.1", "", { "dependencies": { "tinyspy": "^2.2.0" } }, "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw=="], + + "@vitest/utils": ["@vitest/utils@1.6.1", "", { "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" } }, "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], + + "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun": ["bun@1.3.6", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.6", "@oven/bun-darwin-x64": "1.3.6", "@oven/bun-darwin-x64-baseline": "1.3.6", "@oven/bun-linux-aarch64": "1.3.6", "@oven/bun-linux-aarch64-musl": "1.3.6", "@oven/bun-linux-x64": "1.3.6", "@oven/bun-linux-x64-baseline": "1.3.6", "@oven/bun-linux-x64-musl": "1.3.6", "@oven/bun-linux-x64-musl-baseline": "1.3.6", "@oven/bun-windows-x64": "1.3.6", "@oven/bun-windows-x64-baseline": "1.3.6" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-Tn98GlZVN2WM7+lg/uGn5DzUao37Yc0PUz7yzYHdeF5hd+SmHQGbCUIKE4Sspdgtxn49LunK3mDNBC2Qn6GJjw=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.1.1", "", {}, "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="], + + "csv-parser": ["csv-parser@3.2.0", "", { "bin": { "csv-parser": "bin/csv-parser" } }, "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@4.1.4", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.2.2", "", {}, "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-prettier": ["eslint-config-prettier@9.1.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-func-name": ["get-func-name@2.0.2", "", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-cookie": ["js-cookie@3.0.1", "", {}, "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-fetch-native": ["node-fetch-native@1.0.1", "", {}, "sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@5.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], + + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "snakecase-keys": ["snakecase-keys@5.4.4", "", { "dependencies": { "map-obj": "^4.1.0", "snake-case": "^3.0.4", "type-fest": "^2.5.2" } }, "sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strip-literal": ["strip-literal@2.1.1", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "swr": ["swr@2.2.0", "", { "dependencies": { "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ=="], + + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinypool": ["tinypool@0.8.4", "", {}, "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ=="], + + "tinyspy": ["tinyspy@2.2.1", "", {}, "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], + + "tslib": ["tslib@2.4.1", "", {}, "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.2", "", {}, "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@1.6.1", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA=="], + + "vitest": ["vitest@1.6.1", "", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag=="], + + "webcrypto-core": ["webcrypto-core@1.8.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", "tslib": "^2.7.0" } }, "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@clack/prompts/is-unicode-supported": ["is-unicode-supported@2.1.0", "", { "bundled": true }, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "@peculiar/asn1-schema/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@peculiar/json-schema/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "asn1js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "lower-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "pvtsutils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "snakecase-keys/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "webcrypto-core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + } +} diff --git a/index.ts b/index.ts index 2e8e77f..2222b6c 100755 --- a/index.ts +++ b/index.ts @@ -1,174 +1,31 @@ import { config } from "dotenv"; config(); -import * as fs from "fs"; -import * as z from "zod"; -import clerkClient from "@clerk/clerk-sdk-node"; -import ora, { Ora } from "ora"; - -const SECRET_KEY = process.env.CLERK_SECRET_KEY; -const DELAY = parseInt(process.env.DELAY_MS ?? `1_000`); -const RETRY_DELAY = parseInt(process.env.RETRY_DELAY_MS ?? `10_000`); -const IMPORT_TO_DEV = process.env.IMPORT_TO_DEV_INSTANCE ?? "false"; -const OFFSET = parseInt(process.env.OFFSET ?? `0`); - -if (!SECRET_KEY) { - throw new Error( - "CLERK_SECRET_KEY is required. Please copy .env.example to .env and add your key." - ); -} - -if (SECRET_KEY.split("_")[1] !== "live" && IMPORT_TO_DEV === "false") { - throw new Error( - "The Clerk Secret Key provided is for a development instance. Development instances are limited to 500 users and do not share their userbase with production instances. If you want to import users to your development instance, please set 'IMPORT_TO_DEV_INSTANCE' in your .env to 'true'." - ); -} - -const userSchema = z.object({ - /** The ID of the user as used in your external systems or your previous authentication solution. Must be unique across your instance. */ - userId: z.string(), - /** Email address to set as User's primary email address. */ - email: z.string().email(), - /** The first name to assign to the user */ - firstName: z.string().optional(), - /** The last name to assign to the user */ - lastName: z.string().optional(), - /** The plaintext password to give the user. Must be at least 8 characters long, and can not be in any list of hacked passwords. */ - password: z.string().optional(), - /** The hashing algorithm that was used to generate the password digest. - * @see https://clerk.com/docs/reference/backend-api/tag/Users#operation/CreateUser!path=password_hasher&t=request - */ - passwordHasher: z - .enum([ - "argon2i", - "argon2id", - "bcrypt", - "bcrypt_sha256_django", - "ldap_ssha", - "md5", - "md5_phpass", - "pbkdf2_sha256", - "pbkdf2_sha256_django", - "pbkdf2_sha1", - "phpass", - "scrypt_firebase", - "scrypt_werkzeug", - "sha256", - ]) - .optional(), - /** Metadata saved on the user, that is visible to both your Frontend and Backend APIs */ - public_metadata: z.record(z.string(), z.unknown()).optional(), - /** Metadata saved on the user, that is only visible to your Backend APIs */ - private_metadata: z.record(z.string(), z.unknown()).optional(), - /** Metadata saved on the user, that can be updated from both the Frontend and Backend APIs. Note: Since this data can be modified from the frontend, it is not guaranteed to be safe. */ - unsafe_metadata: z.record(z.string(), z.unknown()).optional(), -}); - -type User = z.infer; - -const createUser = (userData: User) => - userData.password - ? clerkClient.users.createUser({ - externalId: userData.userId, - emailAddress: [userData.email], - firstName: userData.firstName, - lastName: userData.lastName, - passwordDigest: userData.password, - passwordHasher: userData.passwordHasher, - privateMetadata: userData.private_metadata, - publicMetadata: userData.public_metadata, - unsafeMetadata: userData.unsafe_metadata, - }) - : clerkClient.users.createUser({ - externalId: userData.userId, - emailAddress: [userData.email], - firstName: userData.firstName, - lastName: userData.lastName, - skipPasswordRequirement: true, - privateMetadata: userData.private_metadata, - publicMetadata: userData.public_metadata, - unsafeMetadata: userData.unsafe_metadata, - }); - -const now = new Date().toISOString().split(".")[0]; // YYYY-MM-DDTHH:mm:ss -function appendLog(payload: any) { - fs.appendFileSync( - `./migration-log-${now}.json`, - `\n${JSON.stringify(payload, null, 2)}` - ); -} - -let migrated = 0; -let alreadyExists = 0; - -async function processUserToClerk(userData: User, spinner: Ora) { - const txt = spinner.text; - try { - const parsedUserData = userSchema.safeParse(userData); - if (!parsedUserData.success) { - throw parsedUserData.error; - } - await createUser(parsedUserData.data); - - migrated++; - } catch (error) { - if (error.status === 422) { - appendLog({ userId: userData.userId, ...error }); - alreadyExists++; - return; - } - - // Keep cooldown in case rate limit is reached as a fallback if the thread blocking fails - if (error.status === 429) { - spinner.text = `${txt} - rate limit reached, waiting for ${RETRY_DELAY} ms`; - await rateLimitCooldown(); - spinner.text = txt; - return processUserToClerk(userData, spinner); - } - - appendLog({ userId: userData.userId, ...error }); - } -} - -async function cooldown() { - await new Promise((r) => setTimeout(r, DELAY)); -} - -async function rateLimitCooldown() { - await new Promise((r) => setTimeout(r, RETRY_DELAY)); +import { env } from "./src/envs-constants"; +import { runCLI } from "./src/cli"; +import { loadUsersFromFile } from "./src/functions"; +import { importUsers } from "./src/import-users"; + +if ( + env.CLERK_SECRET_KEY.split("_")[1] !== "live" && + env.IMPORT_TO_DEV === false +) { + throw new Error( + "The Clerk Secret Key provided is for a development instance. Development instances are limited to 500 users and do not share their userbase with production instances. If you want to import users to your development instance, please set 'IMPORT_TO_DEV' in your .env to 'true'.", + ); } async function main() { - console.log(`Clerk User Migration Utility`); - - const inputFileName = process.argv[2] ?? "users.json"; - - console.log(`Fetching users from ${inputFileName}`); - - const parsedUserData: any[] = JSON.parse( - fs.readFileSync(inputFileName, "utf-8") - ); - const offsetUsers = parsedUserData.slice(OFFSET); - console.log( - `users.json found and parsed, attempting migration with an offset of ${OFFSET}` - ); + const args = await runCLI(); - let i = 0; - const spinner = ora(`Migrating users`).start(); + // we can use Zod to validate the args.keys to ensure it is TransformKeys type + const users = await loadUsersFromFile(args.file, args.key); - for (const userData of offsetUsers) { - spinner.text = `Migrating user ${i}/${offsetUsers.length}, cooldown`; - await cooldown(); - i++; - spinner.text = `Migrating user ${i}/${offsetUsers.length}`; - await processUserToClerk(userData, spinner); - } + const usersToImport = users.slice( + parseInt(args.offset) > env.OFFSET ? parseInt(args.offset) : env.OFFSET, + ); - spinner.succeed(`Migration complete`); - return; + importUsers(usersToImport); } -main().then(() => { - console.log(`${migrated} users migrated`); - console.log(`${alreadyExists} users failed to upload`); -}); +main(); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index e50a911..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1005 +0,0 @@ -{ - "name": "clerk-user-migration", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "clerk-user-migration", - "version": "0.0.1", - "license": "ISC", - "dependencies": { - "@clerk/clerk-sdk-node": "^4.12.21", - "bun": "^1.0.12", - "dotenv": "^16.3.1", - "ora": "^7.0.1", - "zod": "^3.22.4" - }, - "bin": { - "clerk-user-migration": "index.ts" - } - }, - "node_modules/@clerk/backend": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-0.34.1.tgz", - "integrity": "sha512-I6u7vb7XHA0kNek5Ez4VVqBDZKxLepR6wJXlYUy5lGwsTdaQiFwy5Q0nKP2GdQQYtlKpXSAryLu19Cq5zaaNYg==", - "dependencies": { - "@clerk/shared": "1.1.0", - "@clerk/types": "3.58.0", - "@peculiar/webcrypto": "1.4.1", - "@types/node": "16.18.6", - "cookie": "0.5.0", - "deepmerge": "4.2.2", - "node-fetch-native": "1.0.1", - "snakecase-keys": "5.4.4", - "tslib": "2.4.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@clerk/backend/node_modules/snakecase-keys": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-5.4.4.tgz", - "integrity": "sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==", - "dependencies": { - "map-obj": "^4.1.0", - "snake-case": "^3.0.4", - "type-fest": "^2.5.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@clerk/backend/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/@clerk/backend/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@clerk/clerk-sdk-node": { - "version": "4.12.21", - "resolved": "https://registry.npmjs.org/@clerk/clerk-sdk-node/-/clerk-sdk-node-4.12.21.tgz", - "integrity": "sha512-43MdviLlAG3naNzRyxF/Io8YYQBnFEIQiqYFVHzKzZGEsbPST9lBfeFxJZKrCqSE8K7gMx3+3D87bveXq6a7cA==", - "dependencies": { - "@clerk/backend": "0.34.1", - "@clerk/shared": "1.1.0", - "@clerk/types": "3.58.0", - "@types/cookies": "0.7.7", - "@types/express": "4.17.14", - "@types/node-fetch": "2.6.2", - "camelcase-keys": "6.2.2", - "snakecase-keys": "3.2.1", - "tslib": "2.4.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@clerk/clerk-sdk-node/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/@clerk/shared": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-1.1.0.tgz", - "integrity": "sha512-rxQ6bxAERZsf/dzCU35qt3gRp9+a035Vrre8j8tyT60dbP8PQhXUbeNu+oVqqjpHWeyoWWt6fZGLXbDTXdXx7g==", - "dependencies": { - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.1", - "swr": "2.2.0" - }, - "peerDependencies": { - "react": ">=16" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, - "node_modules/@clerk/types": { - "version": "3.58.0", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-3.58.0.tgz", - "integrity": "sha512-fIsvEM3nYQwViOuYxNVcwEl0WkXW6AdYpSghNBKfOge1kriSSHP++T5rRMJBXy6asl2AEydVlUBKx9drAzqKoA==", - "dependencies": { - "csstype": "3.1.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@clerk/types/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, - "node_modules/@oven/bun-darwin-aarch64": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.0.12.tgz", - "integrity": "sha512-e/iNyt8HXlvDTzyvKUyq+vIUVyID9WykyDvNEcz5jM9bcdwimiAo+VGvRhAWnRkazhDBY5H3DL+ixEGy0ljIGw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-darwin-x64": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.0.12.tgz", - "integrity": "sha512-CWfuYPJ1oObCKskOZeg7aM6ToJgt1LEpIIyaqRiYiVji3lrEcnNVPFUJqj7JlQrchZrcrqRr0duKypVCQ+8Jig==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-darwin-x64-baseline": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.0.12.tgz", - "integrity": "sha512-E/0pWuimJlrSzbk6TLgHHvJ0YkRv6oUT1grvgbJz1zyY5/86tAzbc8N6i37kot3jvJ/qF4pF98DkAK+V5TKOMg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-linux-aarch64": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.0.12.tgz", - "integrity": "sha512-0az/FbWNerffUw4ik2VYq/L1m+YncV1uRj59YJMVgB7Eyo1ykgGAmKM/7bUFNrwO1c8Ydz0vj2oOXeYJzWc1Tg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.0.12.tgz", - "integrity": "sha512-A5PP4JpKVwqtj31ZPOHJlerFyw8zOJKRk6ssk1m0jRaFm0/4tEcpqQzX/pPmZcoFhWKcKDnwSJDUIT5vR0q24w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64-baseline": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.0.12.tgz", - "integrity": "sha512-/sSpuNXbCnNoZ3HHL2veGZWmBqIEeM4skaAMp4rSD+Yf5NbHZXeB4qhj7bp7DTMyRESkScMir1DpJifqNhNd/Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", - "integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==", - "dependencies": { - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.5", - "tslib": "^2.6.2" - } - }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@peculiar/webcrypto": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", - "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.0", - "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.1", - "webcrypto-core": "^1.7.4" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookies": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", - "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", - "dependencies": { - "@types/connect": "*", - "@types/express": "*", - "@types/keygrip": "*", - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.41", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", - "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" - }, - "node_modules/@types/keygrip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.5.tgz", - "integrity": "sha512-M+BUYYOXgiYoab5L98VpOY1PzmDwWcTkqqu4mdluez5qOTDV0MVPChxhRIPeIFxQgSi3+6qjg1PnGFaGlW373g==" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, - "node_modules/@types/node": { - "version": "16.18.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.6.tgz", - "integrity": "sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA==" - }, - "node_modules/@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", - "dependencies": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "node_modules/@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", - "dependencies": { - "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/asn1js": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", - "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", - "dependencies": { - "pvtsutils": "^1.3.2", - "pvutils": "^1.1.3", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/bun": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/bun/-/bun-1.0.12.tgz", - "integrity": "sha512-I0CAJJ0HQcu+hdid1jPpRuG1qAyiToZD2eJ0jOX9FLPvhyQQcul6DjRAlW+N1gk9brovK82sba4GvEQxVdCyUA==", - "cpu": [ - "arm64", - "x64" - ], - "hasInstallScript": true, - "os": [ - "darwin", - "linux" - ], - "bin": { - "bun": "bin/bun", - "bunx": "bin/bun" - }, - "optionalDependencies": { - "@oven/bun-darwin-aarch64": "1.0.12", - "@oven/bun-darwin-x64": "1.0.12", - "@oven/bun-darwin-x64-baseline": "1.0.12", - "@oven/bun-linux-aarch64": "1.0.12", - "@oven/bun-linux-x64": "1.0.12", - "@oven/bun-linux-x64-baseline": "1.0.12" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" - }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/js-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", - "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT", - "peer": true - }, - "node_modules/log-symbols": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", - "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", - "dependencies": { - "chalk": "^5.0.0", - "is-unicode-supported": "^1.1.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-fetch-native": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz", - "integrity": "sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==" - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", - "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^4.0.0", - "cli-spinners": "^2.9.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^1.3.0", - "log-symbols": "^5.1.0", - "stdin-discarder": "^0.1.0", - "string-width": "^6.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pvtsutils": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", - "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", - "dependencies": { - "tslib": "^2.6.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "engines": { - "node": ">=8" - } - }, - "node_modules/react": { - "version": "18.2.0", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/snakecase-keys": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.2.1.tgz", - "integrity": "sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA==", - "dependencies": { - "map-obj": "^4.1.0", - "to-snake-case": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stdin-discarder": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", - "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", - "dependencies": { - "bl": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", - "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^10.2.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/swr": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", - "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", - "dependencies": { - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/to-no-case": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", - "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==" - }, - "node_modules/to-snake-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz", - "integrity": "sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==", - "dependencies": { - "to-space-case": "^1.0.0" - } - }, - "node_modules/to-space-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", - "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", - "dependencies": { - "to-no-case": "^1.0.0" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/webcrypto-core": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", - "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", - "dependencies": { - "@peculiar/asn1-schema": "^2.3.6", - "@peculiar/json-schema": "^1.1.12", - "asn1js": "^3.0.1", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json index 060996b..2c537ae 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,33 @@ "keywords": [], "license": "ISC", "scripts": { - "start": "bun index.ts" + "start": "bun index.ts", + "delete": "bun ./src/delete-users.ts", + "lint": "eslint . --config .eslintrc.js", + "lint:fix": "eslint . --fix --config .eslintrc.js", + "format": "prettier . --write", + "format:test": "prettier .", + "test": "vitest" }, "dependencies": { - "@clerk/clerk-sdk-node": "^4.12.21", - "bun": "^1.0.12", - "dotenv": "^16.3.1", - "ora": "^7.0.1", - "zod": "^3.22.4" + "@clack/prompts": "^0.11.0", + "@clerk/backend": "^2.29.2", + "@clerk/types": "^4.101.10", + "bun": "^1.3.6", + "csv-parser": "^3.2.0", + "dotenv": "^17.2.3", + "mime-types": "^3.0.2", + "picocolors": "^1.1.1", + "zod": "^4.3.5" + }, + "devDependencies": { + "@types/mime-types": "^3.0.1", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "prettier": "^3.7.4", + "vitest": "^4.0.17" } } diff --git a/samples/auth0.json b/samples/auth0.json new file mode 100644 index 0000000..62a4460 --- /dev/null +++ b/samples/auth0.json @@ -0,0 +1,142 @@ +[ + { + "_id":{ + "$oid":"6573765d9fa97e13efcc3221" + }, + "email":"janedoe@clerk.dev", + "username":"janedoe", + "email_verified":false, + "tenant":"dev-5b88se1iuijo6w1e", + "connection":"Username-Password-Authentication", + "passwordHash":"$2b$10$OW1kjlVtGbGk1fbKG1TQeupVc9RyrA1gA4c8NN1uCNzyxMIA7EN.u", + "_tmp_is_unique":true, + "version":"1.1", + "identifiers":[ + { + "type":"email", + "value":"janedoe@clerk.dev", + "verified":false + }, + { + "type":"username", + "value":"janedoe" + } + ], + "last_password_reset":{ + "$date":"2023-12-08T20:44:31.608Z" + } + }, + { + "_id":{ + "$oid":"657353cd18710d662aeb4e9e" + }, + "email":"johndoe@clerk.dev", + "username":"johndoe", + "email_verified":true, + "tenant":"dev-5b88se1iuijo6w1e", + "connection":"Username-Password-Authentication", + "passwordHash":"$2b$10$o1bU5mlWpsft6RQFZeCfh.6.ixhdeH7fdfJCm2U1g.XX4Ojnxc3Hm", + "_tmp_is_unique":true, + "version":"1.1", + "identifiers":[ + { + "type":"email", + "value":"johndoe@clerk.dev", + "verified":true + }, + { + "type":"username", + "value":"johndoe" + } + ] + }, + { + "_id":{ + "$oid":"657250b0d60f4fff8f69198a" + }, + "email":"janehancock@clerk.dev", + "email_verified":false, + "tenant":"dev-5b88se1iuijo6w1e", + "connection":"Username-Password-Authentication", + "passwordHash":"$2b$10$w51uK4SH.5rPhFvb0zvOQ.MUGYPURPIThya9RriGMoPVtIl4KVycS", + "_tmp_is_unique":true, + "version":"1.1", + "identifiers":[ + { + "type":"email", + "value":"janehancock@clerk.dev", + "verified":false + } + ] + }, + { + "_id":{ + "$oid":"6573d4d69fa97e13efcca49f" + }, + "email":"johnhancock@clerk.com", + "username":"johnhancock", + "email_verified":true, + "tenant":"dev-5b88se1iuijo6w1e", + "connection":"Username-Password-Authentication", + "passwordHash":"$2b$10$qQiiDhcEm3krRmTj9a2lb.Q4M4W/dkVFQUm/aj1jNxWljt0HSNecK", + "_tmp_is_unique":true, + "version":"1.1", + "identifiers":[ + { + "type":"email", + "value":"johnhancock@clerk.com", + "verified":true + }, + { + "type":"username", + "value":"johnhancock" + } + ] + }, + { + "_id":{ + "$oid":"6573813ce94488fb5f75e089" + }, + "email":"elmo@clerk.dev", + "username":"elmo", + "email_verified":true, + "tenant":"dev-5b88se1iuijo6w1e", + "connection":"Username-Password-Authentication", + "passwordHash":"$2b$10$4a8p79G/F11ZWS3/NGOf9eP9ExnXb0EGZf2FUPB5Wc0pzEoHQM3g.", + "_tmp_is_unique":true, + "version":"1.1", + "identifiers":[ + { + "type":"email", + "value":"elmo@clerk.dev", + "verified":true + }, + { + "type":"username", + "value":"elmo" + } + ] + }, + { + "_id":{ + "$oid":"6572b8339fa97e13efcb57d1" + }, + "email":"kermitthefrog@gmail.com", + "email_verified":false, + "tenant":"dev-5b88se1iuijo6w1e", + "connection":"Username-Password-Authentication", + "passwordHash":"$2b$10$sWOjJ1dp8tG/5BrSZcAwce1UAca4gJkZShYcBg1CdmW/BLc8HueJO", + "_tmp_is_unique":true, + "version":"1.1", + "identifiers":[ + { + "type":"email", + "value":"kermitthefrog@gmail.com", + "verified":false + } + ], + "last_password_reset":{ + "$date":"2023-12-08T23:14:58.161Z" + } + } +] diff --git a/samples/clerk.csv b/samples/clerk.csv new file mode 100644 index 0000000..33de4d0 --- /dev/null +++ b/samples/clerk.csv @@ -0,0 +1,52 @@ +# Password for users with passwords: Kk4aPMeiaRpAs2OeX1NE +id,first_name,last_name,username,primary_email_address,primary_phone_number,verified_email_addresses,unverified_email_addresses,verified_phone_numbers,unverified_phone_numbers,totp_secret,password_digest,password_hasher +user_2YDryYFVMM1W1plDDKz7Gzf4we6,Jane,Doe,,janedoe@test.com,,janedoe@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2ZZCgLE7kJG2CRBxTZ6YUIvzS10,John,Doe,,johndoe@test.com,,johndoe@test.com,,,,,, +user_2cWszPHuo6P2lCdnhhZbVMfbAIC,John,Hancock,,johnhancock@test.com,,johnhancock@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2cukOsyNsh0J3MCEvrgM6PkoB0I,Jane,Hancock,,janehancock@test.com,,janehancock@test.com,,,,,, +user_2dA1B2C3D4E5F6G7H8I9J0K1L2M,Alice,Smith,,alicesmith@test.com,,alicesmith@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dB2C3D4E5F6G7H8I9J0K1L2M3N,Bob,Johnson,,bobjohnson@test.com,,bobjohnson@test.com,,,,,, +user_2dC3D4E5F6G7H8I9J0K1L2M3N4O,Carol,Williams,,carolwilliams@test.com,,carolwilliams@test.com,,,,,, +user_2dD4E5F6G7H8I9J0K1L2M3N4O5P,David,Brown,,davidbrown@test.com,,davidbrown@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dE5F6G7H8I9J0K1L2M3N4O5P6Q,Emma,Jones,,emmajones@test.com,,emmajones@test.com,,,,,, +user_2dF6G7H8I9J0K1L2M3N4O5P6Q7R,Frank,Garcia,,frankgarcia@test.com,,frankgarcia@test.com,,,,,, +user_2dG7H8I9J0K1L2M3N4O5P6Q7R8S,Grace,Miller,,gracemiller@test.com,,gracemiller@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dH8I9J0K1L2M3N4O5P6Q7R8S9T,Henry,Davis,,henrydavis@test.com,,henrydavis@test.com,,,,,, +user_2dI9J0K1L2M3N4O5P6Q7R8S9T0U,Ivy,Rodriguez,,ivyrodriguez@test.com,,ivyrodriguez@test.com,,,,,, +user_2dJ0K1L2M3N4O5P6Q7R8S9T0U1V,Jack,Martinez,,jackmartinez@test.com,,jackmartinez@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dK1L2M3N4O5P6Q7R8S9T0U1V2W,Kate,Hernandez,,katehernandez@test.com,,katehernandez@test.com,,,,,, +user_2dL2M3N4O5P6Q7R8S9T0U1V2W3X,Liam,Lopez,,liamlope@test.com,,liamlope@test.com,,,,,, +user_2dM3N4O5P6Q7R8S9T0U1V2W3X4Y,Mia,Gonzalez,,miagonzalez@test.com,,miagonzalez@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dN4O5P6Q7R8S9T0U1V2W3X4Y5Z,Noah,Wilson,,noahwilson@test.com,,noahwilson@test.com,,,,,, +user_2dO5P6Q7R8S9T0U1V2W3X4Y5Z6A,Olivia,Anderson,,oliviaanderson@test.com,,oliviaanderson@test.com,,,,,, +user_2dP6Q7R8S9T0U1V2W3X4Y5Z6A7B,Peter,Thomas,,peterthomas@test.com,,peterthomas@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dQ7R8S9T0U1V2W3X4Y5Z6A7B8C,Quinn,Taylor,,quinntaylor@test.com,,quinntaylor@test.com,,,,,, +user_2dR8S9T0U1V2W3X4Y5Z6A7B8C9D,Rachel,Moore,,rachelmoore@test.com,,rachelmoore@test.com,,,,,, +user_2dS9T0U1V2W3X4Y5Z6A7B8C9D0E,Sam,Jackson,,samjackson@test.com,,samjackson@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2dT0U1V2W3X4Y5Z6A7B8C9D0E1F,Tina,Martin,,tinamartin@test.com,,tinamartin@test.com,,,,,, +user_2dU1V2W3X4Y5Z6A7B8C9D0E1F2G,Uma,Lee,,umalee@test.com,,umalee@test.com,,,,,, +user_2dV2W3X4Y5Z6A7B8C9D0E1F2G3H,Victor,Perez,,victorperez@test.com,,victorperez@test.com,,,,,, +user_2dW3X4Y5Z6A7B8C9D0E1F2G3H4I,Wendy,Thompson,,wendythompson@test.com,,wendythompson@test.com,,,,,, +user_2dX4Y5Z6A7B8C9D0E1F2G3H4I5J,Xavier,White,,xavierwhite@test.com,,xavierwhite@test.com,,,,,, +user_2dY5Z6A7B8C9D0E1F2G3H4I5J6K,Yara,Harris,,yaraharris@test.com,,yaraharris@test.com,,,,,, +user_2dZ6A7B8C9D0E1F2G3H4I5J6K7L,Zach,Sanchez,,zachsanchez@test.com,,zachsanchez@test.com,,,,,, +user_2eA7B8C9D0E1F2G3H4I5J6K7L8M,Amy,Clark,,amyclark@test.com,,amyclark@test.com,,,,,$2b$10$U4C0ZY8OG8y41F9LusfKyu3HRMBL0rCZcKBVsXhgr.n8Ou6FPhzO2,bcrypt +user_2eB8C9D0E1F2G3H4I5J6K7L8M9N,Brian,Ramirez,,brianramirez@test.com,,brianramirez@test.com,,,,,, +user_2eC9D0E1F2G3H4I5J6K7L8M9N0O,Chloe,Lewis,,chloelewis@test.com,,chloelewis@test.com,,,,,, +user_2eD0E1F2G3H4I5J6K7L8M9N0O1P,Derek,Robinson,,derekrobinson@test.com,,derekrobinson@test.com,,,,,, +user_2eE1F2G3H4I5J6K7L8M9N0O1P2Q,Elena,Walker,,elenawalker@test.com,,elenawalker@test.com,,,,,, +user_2eF2G3H4I5J6K7L8M9N0O1P2Q3R,Felix,Young,,felixyoung@test.com,,felixyoung@test.com,,,,,, +user_2eG3H4I5J6K7L8M9N0O1P2Q3R4S,Gina,Allen,,ginaallen@test.com,,ginaallen@test.com,,,,,, +user_2eH4I5J6K7L8M9N0O1P2Q3R4S5T,Hugo,King,,hugoking@test.com,,hugoking@test.com,,,,,, +user_2eI5J6K7L8M9N0O1P2Q3R4S5T6U,Iris,Wright,,iriswright@test.com,,iriswright@test.com,,,,,, +user_2eJ6K7L8M9N0O1P2Q3R4S5T6U7V,James,Scott,,jamesscott@test.com,,jamesscott@test.com,,,,,, +user_2eK7L8M9N0O1P2Q3R4S5T6U7V8W,Kelly,Torres,,kellytorres@test.com,,kellytorres@test.com,,,,,, +user_2eL8M9N0O1P2Q3R4S5T6U7V8W9X,Leo,Nguyen,,leonguyen@test.com,,leonguyen@test.com,,,,,, +user_2eM9N0O1P2Q3R4S5T6U7V8W9X0Y,Maya,Hill,,mayahill@test.com,,mayahill@test.com,,,,,, +user_2eN0O1P2Q3R4S5T6U7V8W9X0Y1Z,Nate,Flores,,nateflores@test.com,,nateflores@test.com,,,,,, +user_2eO1P2Q3R4S5T6U7V8W9X0Y1Z2A,Ophelia,Green,,opheliagreen@test.com,,opheliagreen@test.com,,,,,, +user_2eP2Q3R4S5T6U7V8W9X0Y1Z2A3B,Paul,Adams,,pauladams@test.com,,pauladams@test.com,,,,,, +user_2eQ3R4S5T6U7V8W9X0Y1Z2A3B4C,Queenie,Nelson,,queenienelson@test.com,,queenienelson@test.com,,,,,, +user_2eR4S5T6U7V8W9X0Y1Z2A3B4C5D,Ryan,Baker,,ryanbaker@test.com,,ryanbaker@test.com,,,,,, +user_2eS5T6U7V8W9X0Y1Z2A3B4C5D6E,Sara,Hall,,sarahall@test.com,,sarahall@test.com,,,,,, +user_2eT6U7V8W9X0Y1Z2A3B4C5D6E7F,Tom,Rivera,,tomrivera@test.com,,tomrivera@test.com,,,,,, diff --git a/samples/clerk.json b/samples/clerk.json new file mode 100644 index 0000000..65a45fb --- /dev/null +++ b/samples/clerk.json @@ -0,0 +1,61 @@ +[ + { + "id": "user_2fT3OpCuU3elx0CXE3cNyStBC9u", + "first_name": "John", + "last_name": "Doe", + "username": null, + "primary_email_address": "johndoe@gmail.com", + "email_addresses": [ + "johndoe@gmail.com", "test@gmail.com" + ], + "primary_phone_number": null, + "phone_numbers": null, + "password_digest": null, + "password_hasher": null, + "unsafe_metadata": { + "username": "johndoe" + }, + "public_metadata": { + "username": "johndoe" + }, + "private_metadata": { + "username": "johndoe" + }, + "has_image": true, + "image_url": "https://storage.googleapis.com/images.clerk.dev/oauth_google/img_2fT3OnxW5K5bLcar5WWBq7Kdrlu", + "mfa_enabled": false, + "backup_codes_enabled": false, + "backup_codes": null, + "totp_secret": null + }, + { + "id": "user_2fTPmPJJGj6SZV1e8xN7yapuoim", + "first_name": "Jane", + "last_name": "Doe", + "username": null, + "primary_email_address": "janedoe@gmail.com", + "email_addresses": [ + "test2@gmail.com", "janedoe@gmail.com" + ], + "primary_phone_number": null, + "phone_numbers": null, + "password_digest": null, + "password_hasher": null, + "unsafe_metadata": { + "username": "janedoe" + }, + "public_metadata": { + "username": "janedoe" + }, + "private_metadata": { + "username": "janedoe" + }, + "has_image": true, + "image_url": "https://img.clerk.com/eyJ0eXBlIjoicHJveHkiLCJzcmMiOiJodHRwczovL2ltYWdlcy5jbGVyay5kZXYvb2F1dGhfZ29vZ2xlL2ltZ18yaENhZFlib0pDbWNiOUlmTHFkREJ5Q2twUkEifQ", + "mfa_enabled": false, + "backup_codes_enabled": false, + "backup_codes": null, + "totp_secret": null + } +] + diff --git a/samples/supabase.csv b/samples/supabase.csv new file mode 100644 index 0000000..d4436c2 --- /dev/null +++ b/samples/supabase.csv @@ -0,0 +1,3 @@ +"instance_id","id","aud","role","email","encrypted_password","email_confirmed_at","invited_at","confirmation_token","confirmation_sent_at","recovery_token","recovery_sent_at","email_change_token_new","email_change","email_change_sent_at","last_sign_in_at","raw_app_meta_data","raw_user_meta_data","is_super_admin","created_at","updated_at","phone","phone_confirmed_at","phone_change","phone_change_token","phone_change_sent_at","confirmed_at","email_change_token_current","email_change_confirm_status","banned_until","reauthentication_token","reauthentication_sent_at","is_sso_user","deleted_at" +"00000000-0000-0000-0000-000000000000","76b196c8-d5c4-4907-9746-ed06ef829a67","authenticated","authenticated","test@test.com","$2a$10$9zQjO8IH4gX/jBn2j8WvquwtBrj8tK7t6FdGsx9nb7e8HzILjxl1m","2024-02-26 14:04:29.153624+00","","","","","","","","","","{""provider"":""email"",""providers"":[""email""]}","{}","","2024-02-26 14:04:29.140992+00","2024-02-26 14:04:29.154469+00","","","","","","2024-02-26 14:04:29.153624+00","","0","","","","false","" +"00000000-0000-0000-0000-000000000000","926f3b49-9687-4d05-8557-2673387a1f3c","authenticated","authenticated","test2@test2.com","$2a$10$4n9B5uDN1pV0m7xUAzRnsuZkEBnGBTQF7kr7u8/tmTMBDOZM2.yBy","2024-03-04 12:12:24.9778+00","","","","","","","","","","{""provider"":""email"",""providers"":[""email""]}","{}","","2024-03-04 12:12:24.968657+00","2024-03-04 12:12:24.978022+00","","","","","","2024-03-04 12:12:24.9778+00","","0","","","","false","" \ No newline at end of file diff --git a/samples/supabase.json b/samples/supabase.json new file mode 100644 index 0000000..74f8ebc --- /dev/null +++ b/samples/supabase.json @@ -0,0 +1,36 @@ +[ +{ + "instance_id": "00000000-0000-0000-0000-000000000000", + "id": "2971a33d-5b7c-4c11-b8fe-61b7f185f211", + "aud": "authenticated", + "role": "authenticated", + "email": "janedoe@clerk.dev", + "encrypted_password": "$2a$10$hg4EXrEHfcqoKhNtENsYCO5anpp/C9WCUAAAtXEqpZkdCcxL/hcGG", + "email_confirmed_at": "2024-02-22 14:34:45.631743+00", + "raw_app_meta_data": "{\"provider\":\"email\",\"providers\":[\"email\"]}", + "raw_user_meta_data": "{}", + "created_at": "2024-02-22 14:34:45.626071+00", + "updated_at": "2024-02-22 14:34:45.631967+00", + "confirmed_at": "2024-02-22 14:34:45.631743+00", + "email_change_confirm_status": "0", + "is_sso_user": "false", + "deleted_at": "" + }, +{ + "instance_id": "00000000-0000-0000-0000-000000000000", + "id": "2971a33d-5b7c-4c11-b8fe-61b7f185f234", + "aud": "authenticated", + "role": "authenticated", + "email": "johndoe@clerk.dev", + "encrypted_password": "$2a$10$hg4EXrEHfcqoKhNtENsYCO5anpp/C9WCUAAAtXEqpZkdCcxL/hcGG", + "email_confirmed_at": "2024-01-01 14:34:45.631743+00", + "raw_app_meta_data": "{\"provider\":\"email\",\"providers\":[\"email\"]}", + "raw_user_meta_data": "{}", + "created_at": "2024-02-22 14:34:45.626071+00", + "updated_at": "2024-02-22 14:34:45.631967+00", + "confirmed_at": "2024-02-22 14:34:45.631743+00", + "email_change_confirm_status": "0", + "is_sso_user": "false", + "deleted_at": "" + } +] diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..d620253 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,448 @@ +import * as p from "@clack/prompts"; +import color from "picocolors"; +import fs from "fs"; +import path from "path"; +import csvParser from "csv-parser"; +import { handlers } from "./handlers"; +import { checkIfFileExists, getFileType, createImportFilePath } from "./utils"; +import { env } from "./envs-constants"; + +const SETTINGS_FILE = ".settings"; + +type Settings = { + key?: string; + file?: string; + offset?: string; +}; + +const DEV_USER_LIMIT = 500; + +const detectInstanceType = (): "dev" | "prod" => { + const secretKey = env.CLERK_SECRET_KEY; + if (secretKey.startsWith("sk_test_")) { + return "dev"; + } + return "prod"; +}; + +// Fields to analyze for the import (non-identifier fields) +const ANALYZED_FIELDS = [ + { key: "firstName", label: "First Name" }, + { key: "lastName", label: "Last Name" }, + { key: "password", label: "Password" }, + { key: "mfaEnabled", label: "MFA Enabled" }, + { key: "totpSecret", label: "TOTP Secret" }, +]; + +type IdentifierCounts = { + verifiedEmails: number; + unverifiedEmails: number; + verifiedPhones: number; + unverifiedPhones: number; + username: number; + hasAnyIdentifier: number; +}; + +type FieldAnalysis = { + presentOnAll: string[]; + presentOnSome: string[]; + identifiers: IdentifierCounts; + totalUsers: number; +}; + +const loadSettings = (): Settings => { + try { + const settingsPath = path.join(process.cwd(), SETTINGS_FILE); + if (fs.existsSync(settingsPath)) { + const content = fs.readFileSync(settingsPath, "utf-8"); + return JSON.parse(content); + } + } catch { + // If settings file is corrupted or unreadable, return empty settings + } + return {}; +}; + +const saveSettings = (settings: Settings): void => { + try { + const settingsPath = path.join(process.cwd(), SETTINGS_FILE); + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + } catch { + // Silently fail if we can't write settings + } +}; + +const loadRawUsers = async (file: string, handlerKey: string): Promise[]> => { + const filePath = createImportFilePath(file); + const type = getFileType(filePath); + const handler = handlers.find((h) => h.key === handlerKey); + + if (!handler) { + throw new Error(`Handler not found for key: ${handlerKey}`); + } + + // Helper to transform keys using handler + const transformKeys = (data: Record): Record => { + const transformed: Record = {}; + const transformer = handler.transformer as Record; + for (const [key, value] of Object.entries(data)) { + if (value !== "" && value !== '"{}"' && value !== null) { + const transformedKey = transformer[key] || key; + transformed[transformedKey] = value; + } + } + return transformed; + }; + + if (type === "text/csv") { + return new Promise((resolve, reject) => { + const users: Record[] = []; + fs.createReadStream(filePath) + .pipe(csvParser({ skipComments: true })) + .on("data", (data) => users.push(transformKeys(data))) + .on("error", (err) => reject(err)) + .on("end", () => resolve(users)); + }); + } else { + const rawUsers = JSON.parse(fs.readFileSync(filePath, "utf-8")); + return rawUsers.map(transformKeys); + } +}; + +const hasValue = (value: unknown): boolean => { + if (value === undefined || value === null || value === "") return false; + if (Array.isArray(value)) return value.length > 0; + return true; +}; + +const analyzeFields = (users: Record[]): FieldAnalysis => { + const totalUsers = users.length; + + if (totalUsers === 0) { + return { + presentOnAll: [], + presentOnSome: [], + identifiers: { + verifiedEmails: 0, + unverifiedEmails: 0, + verifiedPhones: 0, + unverifiedPhones: 0, + username: 0, + hasAnyIdentifier: 0, + }, + totalUsers: 0, + }; + } + + const fieldCounts: Record = {}; + const identifiers: IdentifierCounts = { + verifiedEmails: 0, + unverifiedEmails: 0, + verifiedPhones: 0, + unverifiedPhones: 0, + username: 0, + hasAnyIdentifier: 0, + }; + + // Count how many users have each field + for (const user of users) { + // Count non-identifier fields + for (const field of ANALYZED_FIELDS) { + if (hasValue(user[field.key])) { + fieldCounts[field.key] = (fieldCounts[field.key] || 0) + 1; + } + } + + // Count consolidated identifier fields + const hasVerifiedEmail = hasValue(user.email) || hasValue(user.emailAddresses); + const hasUnverifiedEmail = hasValue(user.unverifiedEmailAddresses); + const hasVerifiedPhone = hasValue(user.phone) || hasValue(user.phoneNumbers); + const hasUnverifiedPhone = hasValue(user.unverifiedPhoneNumbers); + const hasUsername = hasValue(user.username); + + if (hasVerifiedEmail) identifiers.verifiedEmails++; + if (hasUnverifiedEmail) identifiers.unverifiedEmails++; + if (hasVerifiedPhone) identifiers.verifiedPhones++; + if (hasUnverifiedPhone) identifiers.unverifiedPhones++; + if (hasUsername) identifiers.username++; + + // Check if user has at least one valid identifier + if (hasVerifiedEmail || hasVerifiedPhone || hasUsername) { + identifiers.hasAnyIdentifier++; + } + } + + const presentOnAll: string[] = []; + const presentOnSome: string[] = []; + + for (const field of ANALYZED_FIELDS) { + const count = fieldCounts[field.key] || 0; + if (count === totalUsers) { + presentOnAll.push(field.label); + } else if (count > 0) { + presentOnSome.push(field.label); + } + } + + return { presentOnAll, presentOnSome, identifiers, totalUsers }; +}; + +const formatCount = (count: number, total: number, label: string): string => { + if (count === total) { + return `All users have ${label}`; + } else if (count === 0) { + return `No users have ${label}`; + } else { + return `${count} of ${total} users have ${label}`; + } +}; + +const displayIdentifierAnalysis = (analysis: FieldAnalysis): void => { + const { identifiers, totalUsers } = analysis; + + let identifierMessage = ""; + + // Show counts for each identifier type + identifierMessage += color.bold("Identifier Analysis:\n"); + identifierMessage += ` ${identifiers.verifiedEmails === totalUsers ? color.green("●") : identifiers.verifiedEmails > 0 ? color.yellow("○") : color.red("○")} ${formatCount(identifiers.verifiedEmails, totalUsers, "verified emails")}\n`; + identifierMessage += ` ${identifiers.verifiedPhones === totalUsers ? color.green("●") : identifiers.verifiedPhones > 0 ? color.yellow("○") : color.red("○")} ${formatCount(identifiers.verifiedPhones, totalUsers, "verified phone numbers")}\n`; + identifierMessage += ` ${identifiers.username === totalUsers ? color.green("●") : identifiers.username > 0 ? color.yellow("○") : color.red("○")} ${formatCount(identifiers.username, totalUsers, "a username")}\n`; + + // Show unverified counts if present + if (identifiers.unverifiedEmails > 0) { + identifierMessage += ` ${color.dim("○")} ${formatCount(identifiers.unverifiedEmails, totalUsers, "unverified emails")}\n`; + } + if (identifiers.unverifiedPhones > 0) { + identifierMessage += ` ${color.dim("○")} ${formatCount(identifiers.unverifiedPhones, totalUsers, "unverified phone numbers")}\n`; + } + + // Check if all users have at least one identifier + identifierMessage += "\n"; + if (identifiers.hasAnyIdentifier === totalUsers) { + identifierMessage += color.green("All users have at least one identifier (verified email, verified phone, or username).\n"); + } else { + const missing = totalUsers - identifiers.hasAnyIdentifier; + identifierMessage += color.red(`${missing} user${missing === 1 ? " does" : "s do"} not have a verified email, verified phone, or username.\n`); + identifierMessage += color.red("These users cannot be imported.\n"); + } + + // Dashboard configuration advice + identifierMessage += "\n"; + identifierMessage += color.bold("Dashboard Configuration:\n"); + + const requiredIdentifiers: string[] = []; + const optionalIdentifiers: string[] = []; + + if (identifiers.verifiedEmails === totalUsers) { + requiredIdentifiers.push("email"); + } else if (identifiers.verifiedEmails > 0) { + optionalIdentifiers.push("email"); + } + + if (identifiers.verifiedPhones === totalUsers) { + requiredIdentifiers.push("phone"); + } else if (identifiers.verifiedPhones > 0) { + optionalIdentifiers.push("phone"); + } + + if (identifiers.username === totalUsers) { + requiredIdentifiers.push("username"); + } else if (identifiers.username > 0) { + optionalIdentifiers.push("username"); + } + + if (requiredIdentifiers.length > 0) { + identifierMessage += ` ${color.green("●")} Enable and ${color.bold("require")} ${requiredIdentifiers.join(", ")} in the Dashboard\n`; + } + if (optionalIdentifiers.length > 0) { + identifierMessage += ` ${color.yellow("○")} Enable ${optionalIdentifiers.join(", ")} in the Dashboard (do not require)\n`; + } + + p.note(identifierMessage.trim(), "Identifiers"); +}; + +const displayOtherFieldsAnalysis = (analysis: FieldAnalysis): boolean => { + let fieldsMessage = ""; + + if (analysis.presentOnAll.length > 0) { + fieldsMessage += color.bold("Fields present on ALL users:\n"); + fieldsMessage += color.dim("These fields must be enabled in the Clerk Dashboard and could be set as required."); + for (const field of analysis.presentOnAll) { + fieldsMessage += `\n ${color.green("●")} ${color.reset(field)}`; + } + } + + if (analysis.presentOnSome.length > 0) { + if (fieldsMessage) fieldsMessage += "\n\n"; + fieldsMessage += color.bold("Fields present on SOME users:\n"); + fieldsMessage += color.dim("These fields must be enabled in the Clerk Dashboard but must be set as optional."); + for (const field of analysis.presentOnSome) { + fieldsMessage += `\n ${color.yellow("○")} ${color.reset(field)}`; + } + } + + // Add note about passwords + const hasPasswordField = analysis.presentOnAll.includes("Password") || analysis.presentOnSome.includes("Password"); + if (hasPasswordField) { + fieldsMessage += "\n"; + fieldsMessage += color.dim("Note: Passwords can be optional even if not present on all users.\n"); + fieldsMessage += color.dim("The script will use skipPasswordRequirement for users without passwords.\n"); + } + + if (fieldsMessage) { + p.note(fieldsMessage.trim(), "Other Fields"); + return true; + } + + return false; +}; + +export const runCLI = async () => { + p.intro(`${color.bgCyan(color.black("Clerk User Migration Utility"))}`); + + // Load previous settings to use as defaults + const savedSettings = loadSettings(); + + // Step 1: Gather initial inputs + const initialArgs = await p.group( + { + key: () => + p.select({ + message: "What platform are you migrating your users from?", + initialValue: savedSettings.key || handlers[0].value, + maxItems: 1, + options: handlers, + }), + file: () => + p.text({ + message: "Specify the file to use for importing your users", + initialValue: savedSettings.file || "users.json", + placeholder: savedSettings.file || "users.json", + validate: (value) => { + if (!checkIfFileExists(value)) { + return "That file does not exist. Please try again"; + } + if ( + getFileType(value) !== "text/csv" && + getFileType(value) !== "application/json" + ) { + return "Please supply a valid JSON or CSV file"; + } + }, + }), + offset: () => + p.text({ + message: "Specify an offset to begin importing from.", + initialValue: savedSettings.offset || "0", + defaultValue: savedSettings.offset || "0", + placeholder: savedSettings.offset || "0", + }), + }, + { + onCancel: () => { + p.cancel("Migration cancelled."); + process.exit(0); + }, + }, + ); + + // Step 2: Analyze the file and display field information + const spinner = p.spinner(); + spinner.start("Analyzing import file..."); + + let analysis: FieldAnalysis; + let userCount: number; + try { + const users = await loadRawUsers(initialArgs.file, initialArgs.key); + userCount = users.length; + spinner.stop(`Found ${userCount} users in file`); + + analysis = analyzeFields(users); + } catch (error) { + spinner.stop("Error analyzing file"); + p.cancel("Failed to analyze import file. Please check the file format."); + process.exit(1); + } + + // Step 3: Check instance type and validate + const instanceType = detectInstanceType(); + + if (instanceType === "dev") { + p.log.info(`${color.cyan("Development")} instance detected (based on CLERK_SECRET_KEY)`); + + if (userCount > DEV_USER_LIMIT) { + p.cancel( + `Cannot import ${userCount} users to a development instance. ` + + `Development instances are limited to ${DEV_USER_LIMIT} users.` + ); + process.exit(1); + } + } else { + p.log.warn(`${color.yellow("Production")} instance detected (based on CLERK_SECRET_KEY)`); + p.log.warn(color.yellow(`You are about to import ${userCount} users to your production instance.`)); + + const confirmProduction = await p.confirm({ + message: "Are you sure you want to import users to production?", + initialValue: false, + }); + + if (p.isCancel(confirmProduction) || !confirmProduction) { + p.cancel("Migration cancelled."); + process.exit(0); + } + } + + // Step 4: Display and confirm identifier settings + displayIdentifierAnalysis(analysis); + + // Exit if no users have valid identifiers + if (analysis.identifiers.hasAnyIdentifier === 0) { + p.cancel("No users can be imported. All users are missing a valid identifier (verified email, verified phone, or username)."); + process.exit(1); + } + + const confirmIdentifiers = await p.confirm({ + message: "Have you configured the identifier settings in the Dashboard?", + initialValue: true, + }); + + if (p.isCancel(confirmIdentifiers) || !confirmIdentifiers) { + p.cancel("Migration cancelled. Please configure identifier settings and try again."); + process.exit(0); + } + + // Step 5: Display and confirm other field settings (if any) + const hasOtherFields = displayOtherFieldsAnalysis(analysis); + + if (hasOtherFields) { + const confirmFields = await p.confirm({ + message: "Have you configured the field settings in the Dashboard?", + initialValue: true, + }); + + if (p.isCancel(confirmFields) || !confirmFields) { + p.cancel("Migration cancelled. Please configure field settings and try again."); + process.exit(0); + } + } + + // Step 6: Final confirmation + const beginMigration = await p.confirm({ + message: "Begin Migration?", + initialValue: true, + }); + + if (p.isCancel(beginMigration) || !beginMigration) { + p.cancel("Migration cancelled."); + process.exit(0); + } + + // Save settings for next run (not including instance - always auto-detected) + saveSettings({ + key: initialArgs.key, + file: initialArgs.file, + offset: initialArgs.offset, + }); + + return { ...initialArgs, instance: instanceType, begin: beginMigration }; +}; diff --git a/src/delete-users.test.ts b/src/delete-users.test.ts new file mode 100644 index 0000000..8f4d1c1 --- /dev/null +++ b/src/delete-users.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, test, vi, beforeEach, beforeAll } from "vitest"; + +// Mock @clerk/backend before importing the module +const mockGetUserList = vi.fn(); +const mockDeleteUser = vi.fn(); +vi.mock("@clerk/backend", () => ({ + createClerkClient: vi.fn(() => ({ + users: { + getUserList: mockGetUserList, + deleteUser: mockDeleteUser, + }, + })), +})); + +// Mock @clack/prompts to prevent console output during tests +vi.mock("@clack/prompts", () => ({ + intro: vi.fn(), + outro: vi.fn(), + spinner: vi.fn(() => ({ + start: vi.fn(), + stop: vi.fn(), + message: vi.fn(), + })), +})); + +// Mock picocolors +vi.mock("picocolors", () => ({ + default: { + bgCyan: vi.fn((s) => s), + black: vi.fn((s) => s), + }, +})); + +// Mock cooldown to speed up tests +vi.mock("./utils", async () => { + const actual = await vi.importActual("./utils"); + return { + ...actual, + cooldown: vi.fn(() => Promise.resolve()), + }; +}); + +// Mock env constants +vi.mock("./envs-constants", () => ({ + env: { + CLERK_SECRET_KEY: "test_secret_key", + }, +})); + +// NOTE: delete-users.ts calls processUsers() at module level (line 63), +// which makes isolated testing difficult. These tests verify the module +// loads correctly with mocks and the basic structure is testable. +// For full integration testing, the auto-execution should be removed +// from the module and called explicitly from the CLI entry point. + +describe("delete-users module", () => { + beforeAll(() => { + // Setup default mock responses before module loads + mockGetUserList.mockResolvedValue({ + data: [ + { id: "user_1", firstName: "John" }, + { id: "user_2", firstName: "Jane" }, + ], + totalCount: 2, + }); + mockDeleteUser.mockResolvedValue({}); + }); + + test("module exports processUsers function", async () => { + const module = await import("./delete-users"); + expect(module.processUsers).toBeDefined(); + expect(typeof module.processUsers).toBe("function"); + }); + + test("getUserList is called when module executes", async () => { + // Module auto-executes processUsers() on import + await import("./delete-users"); + + // Wait for async operations to complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(mockGetUserList).toHaveBeenCalled(); + expect(mockGetUserList).toHaveBeenCalledWith( + expect.objectContaining({ + offset: 0, + limit: 500, + }) + ); + }); + + test("deleteUser is called for fetched users", async () => { + await import("./delete-users"); + + // Wait for async operations to complete + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Should attempt to delete the users returned by getUserList + expect(mockDeleteUser).toHaveBeenCalled(); + }); +}); + +describe("delete-users behavior documentation", () => { + // These tests document expected behavior for when the module + // is refactored to not auto-execute + + test.todo("fetchUsers should paginate when users exceed LIMIT (500)"); + // Implementation: getUserList should be called multiple times + // with increasing offsets until all users are fetched + + test.todo("fetchUsers should include cooldown between pagination requests"); + // Implementation: cooldown(1000) should be called between pages + + test.todo("deleteUsers should delete all users sequentially"); + // Implementation: deleteUser should be called for each user + // with cooldown between each deletion + + test.todo("deleteUsers should update progress counter correctly"); + // Implementation: spinner.message should show progress [count/total] +}); diff --git a/src/delete-users.ts b/src/delete-users.ts new file mode 100644 index 0000000..b420f8a --- /dev/null +++ b/src/delete-users.ts @@ -0,0 +1,63 @@ +import { createClerkClient, User } from "@clerk/backend"; +import * as p from "@clack/prompts"; +import color from "picocolors"; +import { cooldown } from "./utils"; +import { env } from "./envs-constants"; + +const LIMIT = 500; +const users: User[] = []; +const s = p.spinner(); +let total: number; +let count = 0; + +const fetchUsers = async (offset: number) => { + const clerk = createClerkClient({ secretKey: env.CLERK_SECRET_KEY }) + const { data, totalCount } = await clerk.users.getUserList({ offset, limit: LIMIT }); + + if (data.length > 0) { + for (const user of data) { + users.push(user); + } + } + + if (data.length === LIMIT) { + await cooldown(1000); + return fetchUsers(offset + LIMIT); + } + + return users; +}; + +const deleteUsers = async (users: User[]) => { + s.message(`Deleting users: [0/${total}]`); + for (const user of users) { + const clerk = createClerkClient({ secretKey: env.CLERK_SECRET_KEY }) + await clerk.users.deleteUser(user.id) + .then(async () => { + count++; + s.message(`Deleting users: [${count}/${total}]`); + await cooldown(1000); + }) + } + s.stop(); +}; + +export const processUsers = async () => { + p.intro( + `${color.bgCyan(color.black("Clerk User Migration Utility - Deleting Users"))}`, + ); + + s.start(); + s.message("Fetching current user list"); + const users = await fetchUsers(0); + total = users.length; + + s.stop("Done fetching current user list"); + s.start(); + + await deleteUsers(users); + + p.outro("User deletion complete"); +}; + +processUsers(); diff --git a/src/envs-constants.ts b/src/envs-constants.ts new file mode 100644 index 0000000..fe8881e --- /dev/null +++ b/src/envs-constants.ts @@ -0,0 +1,30 @@ +import { TypeOf, z } from "zod"; +import { config } from "dotenv"; +config(); + +// TODO: Revisit if we need this. Left to easily implement +export const withDevDefault = ( + schema: T, + val: NonNullable>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => (process.env["NODE_ENV"] !== "production" ? schema.default(val as any) : schema); + +const envSchema = z.object({ + CLERK_SECRET_KEY: z.string(), + DELAY: z.coerce.number().optional().default(550), + RETRY_DELAY_MS: z.coerce.number().optional().default(10000), + OFFSET: z.coerce.number().optional().default(0), + IMPORT_TO_DEV: z.coerce.boolean().optional().default(false), +}); + +const parsed = envSchema.safeParse(process.env); + +if (!parsed.success) { + console.error( + "❌ Invalid environment variables:", + JSON.stringify(parsed.error.format(), null, 4), + ); + process.exit(1); +} + +export const env = parsed.data; diff --git a/src/functions.test.ts b/src/functions.test.ts new file mode 100644 index 0000000..4b68e11 --- /dev/null +++ b/src/functions.test.ts @@ -0,0 +1,381 @@ +import { describe, expect, test } from "vitest"; +import { loadUsersFromFile, transformKeys } from "./functions"; +import { handlers } from "./handlers"; + +// test("loadUsersFromFile CSV", async () => { +// const userSupabase = await loadUsersFromFile( +// "/samples/supabase.csv", +// "clerk", +// ); +// +// expect(userSupabase).toMatchInlineSnapshot(` +// [ +// { +// "email": "test@test.com", +// "userId": "76b196c8-d5c4-4907-9746-ed06ef829a67", +// }, +// { +// "email": "test2@test2.com", +// "userId": "926f3b49-9687-4d05-8557-2673387a1f3c", +// }, +// ] +// `); +// }); + +test("Clerk - loadUsersFromFile - JSON", async () => { + const usersFromClerk = await loadUsersFromFile( + "/samples/clerk.json", + "clerk", + ); + + expect(usersFromClerk).toMatchInlineSnapshot(` + [ + { + "backupCodesEnabled": false, + "email": [ + "johndoe@gmail.com", + ], + "firstName": "John", + "lastName": "Doe", + "mfaEnabled": false, + "userId": "user_2fT3OpCuU3elx0CXE3cNyStBC9u", + }, + { + "backupCodesEnabled": false, + "email": [ + "janedoe@gmail.com", + ], + "firstName": "Jane", + "lastName": "Doe", + "mfaEnabled": false, + "userId": "user_2fTPmPJJGj6SZV1e8xN7yapuoim", + }, + ] + `); +}); + +test("Auth.js - loadUsersFromFile - JSON", async () => { + const usersFromAuthjs = await loadUsersFromFile( + "/samples/authjs.json", + "authjs", + ); + + expect(usersFromAuthjs.slice(0, 2)).toMatchInlineSnapshot(` + [ + { + "email": "john@example.com", + "firstName": "John", + "lastName": "Doe", + "password": "$2a$12$9HhLqMJxqBKhlZasxjlhger67GFcC4aOAtpcU.THpcSLiQve4mq6.", + "passwordHasher": "bcrypt", + "userId": "1", + }, + { + "email": "alice@example.com", + "firstName": "Alice", + "lastName": "Smith", + "password": "$2a$12$9HhLqMJxqBKhlZasxjlhger67GFcC4aOAtpcU.THpcSLiQve4mq6.", + "passwordHasher": "bcrypt", + "userId": "2", + }, + ] + `); +}); + +test("Supabase - loadUsersFromFile - JSON", async () => { + const usersFromSupabase = await loadUsersFromFile( + "/samples/supabase.json", + "supabase", + ); + + expect(usersFromSupabase).toMatchInlineSnapshot(` + [ + { + "emailAddresses": "janedoe@clerk.dev", + "password": "$2a$10$hg4EXrEHfcqoKhNtENsYCO5anpp/C9WCUAAAtXEqpZkdCcxL/hcGG", + "passwordHasher": "bcrypt", + "userId": "2971a33d-5b7c-4c11-b8fe-61b7f185f211", + }, + { + "emailAddresses": "johndoe@clerk.dev", + "password": "$2a$10$hg4EXrEHfcqoKhNtENsYCO5anpp/C9WCUAAAtXEqpZkdCcxL/hcGG", + "passwordHasher": "bcrypt", + "userId": "2971a33d-5b7c-4c11-b8fe-61b7f185f234", + }, + ] + `); +}); + +test("Auth0 - loadUsersFromFile - JSON", async () => { + const usersFromAuth0 = await loadUsersFromFile( + "/samples/auth0.json", + "auth0", + ); + + expect(usersFromAuth0).toMatchInlineSnapshot(`[]`); +}); + +// ============================================================================ +// transformKeys tests +// ============================================================================ + +describe("transformKeys", () => { + const clerkHandler = handlers.find((h) => h.key === "clerk")!; + const supabaseHandler = handlers.find((h) => h.key === "supabase")!; + const auth0Handler = handlers.find((h) => h.key === "auth0")!; + + describe("key transformation", () => { + test("transforms keys according to handler config", () => { + const data = { + id: "user_123", + first_name: "John", + last_name: "Doe", + primary_email_address: "john@example.com", + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + firstName: "John", + lastName: "Doe", + email: "john@example.com", + }); + }); + + test("transforms Clerk-specific keys", () => { + const data = { + id: "user_123", + primary_email_address: "john@example.com", + verified_email_addresses: ["john@example.com", "other@example.com"], + password_digest: "$2a$10$hash", + password_hasher: "bcrypt", + mfa_enabled: true, + totp_secret: "SECRET", + backup_codes_enabled: false, + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + email: "john@example.com", + emailAddresses: ["john@example.com", "other@example.com"], + password: "$2a$10$hash", + passwordHasher: "bcrypt", + mfaEnabled: true, + totpSecret: "SECRET", + backupCodesEnabled: false, + }); + }); + + test("transforms Supabase-specific keys", () => { + const data = { + id: "uuid-123", + email: "jane@example.com", + first_name: "Jane", + last_name: "Smith", + encrypted_password: "$2a$10$hash", + phone: "+1234567890", + }; + + const result = transformKeys(data, supabaseHandler); + + expect(result).toEqual({ + userId: "uuid-123", + emailAddresses: "jane@example.com", + firstName: "Jane", + lastName: "Smith", + password: "$2a$10$hash", + phone: "+1234567890", + }); + }); + + test("transforms Auth0-specific keys", () => { + const data = { + id: "auth0|123", + email: "user@example.com", + given_name: "Bob", + family_name: "Jones", + phone_number: "+1987654321", + passwordHash: "$2b$10$hash", + user_metadata: { role: "admin" }, + }; + + const result = transformKeys(data, auth0Handler); + + expect(result).toEqual({ + userId: "auth0|123", + emailAddresses: "user@example.com", + firstName: "Bob", + lastName: "Jones", + phone: "+1987654321", + password: "$2b$10$hash", + publicMetadata: { role: "admin" }, + }); + }); + + test("keeps unmapped keys unchanged", () => { + const data = { + id: "user_123", + customField: "custom value", + anotherField: 42, + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + customField: "custom value", + anotherField: 42, + }); + }); + }); + + describe("filtering empty values", () => { + test("filters out empty strings", () => { + const data = { + id: "user_123", + first_name: "John", + last_name: "", + primary_email_address: "john@example.com", + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + firstName: "John", + email: "john@example.com", + }); + expect(result).not.toHaveProperty("lastName"); + }); + + test("filters out empty JSON string '{\"}'", () => { + const data = { + id: "user_123", + first_name: "John", + public_metadata: '"{}"', + unsafe_metadata: '"{}"', + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + firstName: "John", + }); + expect(result).not.toHaveProperty("publicMetadata"); + expect(result).not.toHaveProperty("unsafeMetadata"); + }); + + test("filters out null values", () => { + const data = { + id: "user_123", + first_name: "John", + last_name: null, + username: null, + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + firstName: "John", + }); + expect(result).not.toHaveProperty("lastName"); + expect(result).not.toHaveProperty("username"); + }); + + test("keeps falsy but valid values (false, 0)", () => { + const data = { + id: "user_123", + mfa_enabled: false, + backup_codes_enabled: false, + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + mfaEnabled: false, + backupCodesEnabled: false, + }); + }); + + test("keeps undefined values (current behavior)", () => { + const data = { + id: "user_123", + first_name: undefined, + }; + + const result = transformKeys(data, clerkHandler); + + // undefined is not filtered, only "", '"{}"', and null + expect(result).toHaveProperty("firstName"); + expect(result.firstName).toBeUndefined(); + }); + }); + + describe("edge cases", () => { + test("handles empty object", () => { + const result = transformKeys({}, clerkHandler); + expect(result).toEqual({}); + }); + + test("handles object with only filtered values", () => { + const data = { + first_name: "", + last_name: null, + username: '"{}"', + }; + + const result = transformKeys(data, clerkHandler); + expect(result).toEqual({}); + }); + + test("preserves array values", () => { + const data = { + id: "user_123", + verified_email_addresses: ["a@example.com", "b@example.com"], + verified_phone_numbers: ["+1111111111", "+2222222222"], + }; + + const result = transformKeys(data, clerkHandler); + + expect(result.emailAddresses).toEqual(["a@example.com", "b@example.com"]); + expect(result.phoneNumbers).toEqual(["+1111111111", "+2222222222"]); + }); + + test("preserves object values", () => { + const data = { + id: "user_123", + public_metadata: { role: "admin", tier: "premium" }, + private_metadata: { internalId: 456 }, + }; + + const result = transformKeys(data, clerkHandler); + + expect(result.publicMetadata).toEqual({ role: "admin", tier: "premium" }); + expect(result.privateMetadata).toEqual({ internalId: 456 }); + }); + + test("handles special characters in values", () => { + const data = { + id: "user_123", + first_name: "José", + last_name: "O'Brien", + username: "user@special!", + }; + + const result = transformKeys(data, clerkHandler); + + expect(result).toEqual({ + userId: "user_123", + firstName: "José", + lastName: "O'Brien", + username: "user@special!", + }); + }); + }); +}); diff --git a/src/functions.ts b/src/functions.ts new file mode 100644 index 0000000..b216745 --- /dev/null +++ b/src/functions.ts @@ -0,0 +1,192 @@ +import fs from "fs"; +import csvParser from "csv-parser"; +import * as p from "@clack/prompts"; +import { validationLogger } from "./logger"; +import { handlers } from "./handlers"; +import { userSchema } from "./validators"; +import { HandlerMapKeys, HandlerMapUnion, User } from "./types"; +import { createImportFilePath, getDateTimeStamp, getFileType } from "./utils"; + +const s = p.spinner(); + +// transform incoming data datas to match default schema +export function transformKeys( + data: Record, + keys: T, +): Record { + const transformedData: Record = {}; + const transformer = keys.transformer as Record; + for (const [key, value] of Object.entries(data)) { + if (value !== "" && value !== '"{}"' && value !== null) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + let transformedKey = key; + if (transformer[key]) transformedKey = transformer[key]; + + transformedData[transformedKey] = data[key]; + } + } + } + return transformedData; +} + +const transformUsers = ( + users: User[], + key: HandlerMapKeys, + dateTime: string, +) => { + // This applies to smaller numbers. Pass in 10, get 5 back. + const transformedData: User[] = []; + for (let i = 0; i < users.length; i++) { + const transformerKeys = handlers.find((obj) => obj.key === key); + + if (transformerKeys === undefined) { + throw new Error("No transformer found for the specified key"); + } + + const transformedUser = transformKeys(users[i], transformerKeys); + + // Transform email to array for clerk handler (merges primary + verified + unverified emails) + if (key === "clerk") { + // Helper to parse email field - could be array (JSON) or comma-separated string (CSV) + const parseEmails = (field: unknown): string[] => { + if (Array.isArray(field)) return field; + if (typeof field === "string" && field) { + return field.split(",").map((e: string) => e.trim()).filter(Boolean); + } + return []; + }; + + const primaryEmail = transformedUser.email as string | undefined; + const verifiedEmails = parseEmails(transformedUser.emailAddresses); + const unverifiedEmails = parseEmails(transformedUser.unverifiedEmailAddresses); + + // Build email array: primary first, then verified, then unverified (deduplicated) + const allEmails: string[] = []; + if (primaryEmail) allEmails.push(primaryEmail); + for (const email of [...verifiedEmails, ...unverifiedEmails]) { + if (!allEmails.includes(email)) allEmails.push(email); + } + if (allEmails.length > 0) { + transformedUser.email = allEmails; + } + + // Helper to parse phone field - could be array (JSON) or comma-separated string (CSV) + const parsePhones = (field: unknown): string[] => { + if (Array.isArray(field)) return field; + if (typeof field === "string" && field) { + return field.split(",").map((p: string) => p.trim()).filter(Boolean); + } + return []; + }; + + const primaryPhone = transformedUser.phone as string | undefined; + const verifiedPhones = parsePhones(transformedUser.phoneNumbers); + const unverifiedPhones = parsePhones(transformedUser.unverifiedPhoneNumbers); + + // Build phone array: primary first, then verified, then unverified (deduplicated) + const allPhones: string[] = []; + if (primaryPhone) allPhones.push(primaryPhone); + for (const phone of [...verifiedPhones, ...unverifiedPhones]) { + if (!allPhones.includes(phone)) allPhones.push(phone); + } + if (allPhones.length > 0) { + transformedUser.phone = allPhones; + } + } + const validationResult = userSchema.safeParse(transformedUser); + // Check if validation was successful + if (validationResult.success) { + // The data is valid according to the original schema + const validatedData = validationResult.data; + transformedData.push(validatedData); + } else { + // The data is not valid, handle errors + const firstIssue = validationResult.error.issues[0]; + validationLogger( + { + error: `${firstIssue.code} for required field.`, + path: firstIssue.path as (string | number)[], + id: transformedUser.userId as string, + row: i, + }, + dateTime, + ); + } + } + return transformedData; +}; + +const addDefaultFields = (users: User[], key: string) => { + const handler = handlers.find((obj) => obj.key === key); + const defaultFields = (handler && "defaults" in handler) ? handler.defaults : null; + + if (defaultFields) { + const updatedUsers: User[] = []; + + for (const user of users) { + const updated = { + ...user, + ...defaultFields, + }; + updatedUsers.push(updated); + } + + return updatedUsers; + } else { + return users; + } +}; + +export const loadUsersFromFile = async ( + file: string, + key: HandlerMapKeys, +): Promise => { + const dateTime = getDateTimeStamp(); + s.start(); + s.message("Loading users and perparing to migrate"); + + const type = getFileType(createImportFilePath(file)); + + // convert a CSV to JSON and return array + if (type === "text/csv") { + const users: User[] = []; + return new Promise((resolve, reject) => { + fs.createReadStream(createImportFilePath(file)) + .pipe(csvParser({ skipComments: true })) + .on("data", (data) => { + users.push(data); + }) + .on("error", (err) => { + s.stop("Error loading users"); + reject(err); + }) + .on("end", () => { + const usersWithDefaultFields = addDefaultFields(users, key); + const transformedData: User[] = transformUsers( + usersWithDefaultFields, + key, + dateTime, + ); + s.stop("Users Loaded"); + resolve(transformedData); + }); + }); + + // if the file is already JSON, just read and parse and return the result + } else { + const users: User[] = JSON.parse( + fs.readFileSync(createImportFilePath(file), "utf-8"), + ); + const usersWithDefaultFields = addDefaultFields(users, key); + + const transformedData: User[] = transformUsers( + usersWithDefaultFields, + key, + dateTime, + ); + + s.stop("Users Loaded"); + // p.log.step('Users loaded') + return transformedData; + } +}; diff --git a/src/handlers.ts b/src/handlers.ts new file mode 100644 index 0000000..a2f8cda --- /dev/null +++ b/src/handlers.ts @@ -0,0 +1,81 @@ +const clerkHandler = { + key: "clerk", + value: "clerk", + label: "Clerk", + transformer: { + id: "userId", + primary_email_address: "email", + verified_email_addresses: "emailAddresses", + unverified_email_addresses: "unverifiedEmailAddresses", + first_name: "firstName", + last_name: "lastName", + password_digest: "password", + password_hasher: "passwordHasher", + primary_phone_number: "phone", + verified_phone_numbers: "phoneNumbers", + unverified_phone_numbers: "unverifiedPhoneNumbers", + username: "username", + mfa_enabled: "mfaEnabled", + totp_secret: "totpSecret", + backup_codes_enabled: "backupCodesEnabled", + backup_codes: "backupCodes", + public_metadata: "publicMetadata", + unsafe_metadata: "unsafeMetadata", + private_metadata: "privateMetadata", + }, +} + +const authjsHandler = { + key: "authjs", + value: "authjs", + label: "Authjs (Next-Auth)", + transformer: { + id: "userId", + email_addresses: "emailAddresses", + first_name: "firstName", + last_name: "lastName", + }, +} + +const supabaseHandler = { + key: "supabase", + value: "supabase", + label: "Supabase", + transformer: { + id: "userId", + email: "emailAddresses", + first_name: "firstName", + last_name: "lastName", + encrypted_password: "password", + phone: "phone", + }, + defaults: { + passwordHasher: "bcrypt" as const, + }, +} + +const auth0Handler = { + key: "auth0", + value: "auth0", + label: "Auth0", + transformer: { + id: "userId", + email: "emailAddresses", + given_name: "firstName", + family_name: "lastName", + phone_number: "phone", + passwordHash: "password", + user_metadata: "publicMetadata", + }, + defaults: { + passwordHasher: "bcrypt" as const, + }, +} + + +export const handlers = [ + clerkHandler, + auth0Handler, + authjsHandler, + supabaseHandler, +]; diff --git a/src/import-users.test.ts b/src/import-users.test.ts new file mode 100644 index 0000000..20b9680 --- /dev/null +++ b/src/import-users.test.ts @@ -0,0 +1,479 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from "vitest"; +import { existsSync, rmSync } from "node:fs"; + +// Mock @clerk/backend before importing the module +const mockCreateUser = vi.fn(); +const mockCreateEmailAddress = vi.fn(); +const mockCreatePhoneNumber = vi.fn(); +vi.mock("@clerk/backend", () => ({ + createClerkClient: vi.fn(() => ({ + users: { + createUser: mockCreateUser, + }, + emailAddresses: { + createEmailAddress: mockCreateEmailAddress, + }, + phoneNumbers: { + createPhoneNumber: mockCreatePhoneNumber, + }, + })), +})); + +// Mock @clack/prompts to prevent console output during tests +vi.mock("@clack/prompts", () => ({ + spinner: vi.fn(() => ({ + start: vi.fn(), + stop: vi.fn(), + message: vi.fn(), + })), + outro: vi.fn(), + note: vi.fn(), +})); + +// Mock cooldown to speed up tests +vi.mock("./utils", async () => { + const actual = await vi.importActual("./utils"); + return { + ...actual, + cooldown: vi.fn(() => Promise.resolve()), + }; +}); + +// Mock env constants +vi.mock("./envs-constants", () => ({ + env: { + CLERK_SECRET_KEY: "test_secret_key", + DELAY: 0, + RETRY_DELAY_MS: 0, + }, +})); + +// Import after mocks are set up +import { importUsers } from "./import-users"; +import * as logger from "./logger"; + +// Helper to clean up logs directory +const cleanupLogs = () => { + if (existsSync("logs")) { + rmSync("logs", { recursive: true }); + } +}; + +describe("importUsers", () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanupLogs(); + }); + + afterEach(() => { + cleanupLogs(); + }); + + describe("createUser API calls", () => { + test("calls Clerk API with correct params for user with password", async () => { + mockCreateUser.mockResolvedValue({ id: "user_created" }); + + const users = [ + { + userId: "user_123", + email: ["john@example.com"], + firstName: "John", + lastName: "Doe", + password: "$2a$10$hashedpassword", + passwordHasher: "bcrypt" as const, + username: "johndoe", + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledTimes(1); + expect(mockCreateUser).toHaveBeenCalledWith({ + externalId: "user_123", + emailAddress: ["john@example.com"], + firstName: "John", + lastName: "Doe", + passwordDigest: "$2a$10$hashedpassword", + passwordHasher: "bcrypt", + username: "johndoe", + phoneNumber: undefined, + totpSecret: undefined, + }); + }); + + test("calls Clerk API with skipPasswordRequirement for user without password", async () => { + mockCreateUser.mockResolvedValue({ id: "user_created" }); + + const users = [ + { + userId: "user_456", + email: ["jane@example.com"], + firstName: "Jane", + lastName: "Smith", + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledTimes(1); + expect(mockCreateUser).toHaveBeenCalledWith({ + externalId: "user_456", + emailAddress: ["jane@example.com"], + firstName: "Jane", + lastName: "Smith", + skipPasswordRequirement: true, + username: undefined, + phoneNumber: undefined, + totpSecret: undefined, + }); + }); + + test("processes multiple users sequentially", async () => { + mockCreateUser.mockResolvedValue({ id: "user_created" }); + + const users = [ + { userId: "user_1", email: ["user1@example.com"] }, + { userId: "user_2", email: ["user2@example.com"] }, + { userId: "user_3", email: ["user3@example.com"] }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledTimes(3); + }); + + test("includes phone number when provided", async () => { + mockCreateUser.mockResolvedValue({ id: "user_created" }); + + const users = [ + { + userId: "user_phone", + email: ["phone@example.com"], + phone: ["+1234567890"], + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + phoneNumber: ["+1234567890"], + }) + ); + }); + + test("includes TOTP secret when provided", async () => { + mockCreateUser.mockResolvedValue({ id: "user_created" }); + + const users = [ + { + userId: "user_totp", + email: ["totp@example.com"], + totpSecret: "JBSWY3DPEHPK3PXP", + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + totpSecret: "JBSWY3DPEHPK3PXP", + }) + ); + }); + }); + + describe("error handling", () => { + test("logs error when Clerk API fails", async () => { + const errorLoggerSpy = vi.spyOn(logger, "errorLogger"); + + const clerkError = { + status: 422, + errors: [ + { + code: "form_identifier_exists", + message: "Email exists", + longMessage: "That email address is taken.", + }, + ], + }; + mockCreateUser.mockRejectedValue(clerkError); + + const users = [ + { userId: "user_fail", email: ["existing@example.com"] }, + ]; + + await importUsers(users); + + expect(errorLoggerSpy).toHaveBeenCalled(); + expect(errorLoggerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + userId: "user_fail", + status: "422", + }), + expect.any(String) + ); + }); + + test("continues processing after error", async () => { + mockCreateUser + .mockRejectedValueOnce({ + status: 400, + errors: [{ code: "error", message: "Failed" }], + }) + .mockResolvedValueOnce({ id: "user_2_created" }) + .mockResolvedValueOnce({ id: "user_3_created" }); + + const users = [ + { userId: "user_1", email: ["user1@example.com"] }, + { userId: "user_2", email: ["user2@example.com"] }, + { userId: "user_3", email: ["user3@example.com"] }, + ]; + + await importUsers(users); + + // All three should be attempted + expect(mockCreateUser).toHaveBeenCalledTimes(3); + }); + + test("retries on rate limit (429) error", async () => { + const rateLimitError = { + status: 429, + errors: [{ code: "rate_limit", message: "Too many requests" }], + }; + + mockCreateUser + .mockRejectedValueOnce(rateLimitError) + .mockResolvedValueOnce({ id: "user_created" }); + + const users = [ + { userId: "user_rate", email: ["rate@example.com"] }, + ]; + + await importUsers(users); + + // Should be called twice: first fails with 429, retry succeeds + expect(mockCreateUser).toHaveBeenCalledTimes(2); + }); + }); + + describe("validation", () => { + test("skips createUser for invalid users (missing userId)", async () => { + // Mock errorLogger to prevent TypeError from ZodError structure mismatch + vi.spyOn(logger, "errorLogger").mockImplementation(() => {}); + + const users = [ + { email: ["noid@example.com"] } as any, + ]; + + await importUsers(users); + + // createUser should not be called for invalid user + expect(mockCreateUser).not.toHaveBeenCalled(); + }); + }); +}); + +describe("importUsers edge cases", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockCreatePhoneNumber.mockReset(); + cleanupLogs(); + }); + + afterEach(() => { + cleanupLogs(); + }); + + test("handles empty user array", async () => { + await importUsers([]); + expect(mockCreateUser).not.toHaveBeenCalled(); + }); + + test("handles user with all optional fields", async () => { + mockCreateUser.mockResolvedValue({ id: "user_full_created" }); + mockCreateEmailAddress.mockResolvedValue({}); + + const users = [ + { + userId: "user_full", + email: ["full@example.com", "secondary@example.com"], + firstName: "Full", + lastName: "User", + password: "$2a$10$hash", + passwordHasher: "bcrypt" as const, + username: "fulluser", + phone: ["+1111111111"], + totpSecret: "SECRET123", + mfaEnabled: true, + backupCodesEnabled: true, + }, + ]; + + await importUsers(users); + + // createUser should be called with only the primary email + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + externalId: "user_full", + emailAddress: ["full@example.com"], + firstName: "Full", + lastName: "User", + passwordDigest: "$2a$10$hash", + passwordHasher: "bcrypt", + username: "fulluser", + phoneNumber: ["+1111111111"], + totpSecret: "SECRET123", + }) + ); + + // createEmailAddress should be called for additional emails + expect(mockCreateEmailAddress).toHaveBeenCalledWith({ + userId: "user_full_created", + emailAddress: "secondary@example.com", + primary: false, + }); + }); + + test("adds multiple additional emails after user creation", async () => { + mockCreateUser.mockResolvedValue({ id: "user_multi_email" }); + mockCreateEmailAddress.mockResolvedValue({}); + + const users = [ + { + userId: "user_emails", + email: ["primary@example.com", "second@example.com", "third@example.com"], + }, + ]; + + await importUsers(users); + + // createUser gets only the first email + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + emailAddress: ["primary@example.com"], + }) + ); + + // createEmailAddress called for each additional email + expect(mockCreateEmailAddress).toHaveBeenCalledTimes(2); + expect(mockCreateEmailAddress).toHaveBeenCalledWith({ + userId: "user_multi_email", + emailAddress: "second@example.com", + primary: false, + }); + expect(mockCreateEmailAddress).toHaveBeenCalledWith({ + userId: "user_multi_email", + emailAddress: "third@example.com", + primary: false, + }); + }); + + test("does not call createEmailAddress when only one email", async () => { + mockCreateUser.mockResolvedValue({ id: "user_single" }); + + const users = [ + { + userId: "user_one_email", + email: ["only@example.com"], + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledTimes(1); + expect(mockCreateEmailAddress).not.toHaveBeenCalled(); + }); + + test("adds multiple additional phones after user creation", async () => { + mockCreateUser.mockResolvedValue({ id: "user_multi_phone" }); + mockCreatePhoneNumber.mockResolvedValue({}); + + const users = [ + { + userId: "user_phones", + email: ["test@example.com"], + phone: ["+1111111111", "+2222222222", "+3333333333"], + }, + ]; + + await importUsers(users); + + // createUser gets only the first phone + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + phoneNumber: ["+1111111111"], + }) + ); + + // createPhoneNumber called for each additional phone + expect(mockCreatePhoneNumber).toHaveBeenCalledTimes(2); + expect(mockCreatePhoneNumber).toHaveBeenCalledWith({ + userId: "user_multi_phone", + phoneNumber: "+2222222222", + primary: false, + }); + expect(mockCreatePhoneNumber).toHaveBeenCalledWith({ + userId: "user_multi_phone", + phoneNumber: "+3333333333", + primary: false, + }); + }); + + test("does not call createPhoneNumber when only one phone", async () => { + mockCreateUser.mockResolvedValue({ id: "user_single_phone" }); + + const users = [ + { + userId: "user_one_phone", + email: ["test@example.com"], + phone: ["+1234567890"], + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledTimes(1); + expect(mockCreatePhoneNumber).not.toHaveBeenCalled(); + }); + + test("handles phone as string (converts to array)", async () => { + mockCreateUser.mockResolvedValue({ id: "user_string_phone" }); + + const users = [ + { + userId: "user_string_phone", + email: ["test@example.com"], + phone: "+1234567890", + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + phoneNumber: ["+1234567890"], + }) + ); + expect(mockCreatePhoneNumber).not.toHaveBeenCalled(); + }); + + test("handles user without phone", async () => { + mockCreateUser.mockResolvedValue({ id: "user_no_phone" }); + + const users = [ + { + userId: "user_no_phone", + email: ["test@example.com"], + }, + ]; + + await importUsers(users); + + expect(mockCreateUser).toHaveBeenCalledWith( + expect.not.objectContaining({ + phoneNumber: expect.anything(), + }) + ); + }); +}); diff --git a/src/import-users.ts b/src/import-users.ts new file mode 100644 index 0000000..08d4116 --- /dev/null +++ b/src/import-users.ts @@ -0,0 +1,184 @@ +import { createClerkClient } from "@clerk/backend"; +import { ClerkAPIError } from "@clerk/types"; +import { env } from "./envs-constants"; +import * as p from "@clack/prompts"; +import color from "picocolors"; +import { errorLogger, importLogger } from "./logger"; +import { cooldown, getDateTimeStamp } from "./utils"; +import { userSchema } from "./validators"; +import { ImportSummary, User } from "./types"; + +const s = p.spinner(); +let processed = 0; +let successful = 0; +let failed = 0; +const errorCounts = new Map(); + +const createUser = async (userData: User) => { + const clerk = createClerkClient({ secretKey: env.CLERK_SECRET_KEY }); + + // Extract primary email and additional emails + const emails = userData.email + ? (Array.isArray(userData.email) ? userData.email : [userData.email]) + : []; + const primaryEmail = emails[0]; + const additionalEmails = emails.slice(1); + + // Extract primary phone and additional phones + const phones = userData.phone + ? (Array.isArray(userData.phone) ? userData.phone : [userData.phone]) + : []; + const primaryPhone = phones[0]; + const additionalPhones = phones.slice(1); + + // Build user params dynamically based on available fields + // Using Record type to allow dynamic property assignment for password hashing params + const userParams: Record = { + externalId: userData.userId, + }; + + // Add email if present + if (primaryEmail) userParams.emailAddress = [primaryEmail]; + + // Add optional fields only if they have values + if (userData.firstName) userParams.firstName = userData.firstName; + if (userData.lastName) userParams.lastName = userData.lastName; + if (userData.username) userParams.username = userData.username; + if (primaryPhone) userParams.phoneNumber = [primaryPhone]; + if (userData.totpSecret) userParams.totpSecret = userData.totpSecret; + // if (userData.unsafeMetadata) userParams.unsafeMetadata = userData.unsafeMetadata; + // if (userData.privateMetadata) userParams.privateMetadata = userData.privateMetadata; + // if (userData.publicMetadata) userParams.publicMetadata = userData.publicMetadata; + + // Handle password - if present, include digest and hasher; otherwise skip password requirement + if (userData.password && userData.passwordHasher) { + userParams.passwordDigest = userData.password; + userParams.passwordHasher = userData.passwordHasher; + } else { + userParams.skipPasswordRequirement = true; + } + + // Create the user with the primary email + const createdUser = await clerk.users.createUser( + userParams as Parameters[0] + ); + + // Add additional emails to the created user + for (const email of additionalEmails) { + if (email) { + await clerk.emailAddresses.createEmailAddress({ + userId: createdUser.id, + emailAddress: email, + primary: false, + }); + } + } + + // Add additional phones to the created user + for (const phone of additionalPhones) { + if (phone) { + await clerk.phoneNumbers.createPhoneNumber({ + userId: createdUser.id, + phoneNumber: phone, + primary: false, + }); + } + } + + return createdUser; +}; + +async function processUserToClerk( + userData: User, + total: number, + dateTime: string, +) { + try { + const parsedUserData = userSchema.safeParse(userData); + if (!parsedUserData.success) { + throw parsedUserData.error; + } + await createUser(parsedUserData.data); + successful++; + processed++; + s.message(`Migrating users: [${processed}/${total}]`); + + // Log successful import + importLogger( + { userId: userData.userId, status: "success" }, + dateTime, + ); + } catch (error: unknown) { + // Keep cooldown in case rate limit is reached as a fallback if the thread blocking fails + const clerkError = error as { status?: number; errors?: ClerkAPIError[] }; + if (clerkError.status === 429) { + await cooldown(env.RETRY_DELAY_MS); + return processUserToClerk(userData, total, dateTime); + } + + // Track error for summary + failed++; + processed++; + s.message(`Migrating users: [${processed}/${total}]`); + + const errorMessage = clerkError.errors?.[0]?.longMessage ?? clerkError.errors?.[0]?.message ?? "Unknown error"; + errorCounts.set(errorMessage, (errorCounts.get(errorMessage) ?? 0) + 1); + + // Log to error log file + errorLogger( + { userId: userData.userId, status: String(clerkError.status ?? "unknown"), errors: clerkError.errors ?? [] }, + dateTime, + ); + + // Log to import log file + importLogger( + { userId: userData.userId, status: "error", error: errorMessage }, + dateTime, + ); + } +} + +const displaySummary = (summary: ImportSummary) => { + let message = color.bold("Migration Summary\n\n"); + message += ` Total users processed: ${summary.totalProcessed}\n`; + message += ` ${color.green("Successfully imported:")} ${summary.successful}\n`; + message += ` ${color.red("Failed with errors:")} ${summary.failed}\n`; + + if (summary.errorBreakdown.size > 0) { + message += `\n${color.bold("Error Breakdown:")}\n`; + for (const [error, count] of summary.errorBreakdown) { + message += ` ${color.red("•")} ${count} user${count === 1 ? "" : "s"}: ${error}\n`; + } + } + + p.note(message.trim(), "Complete"); +}; + +export const importUsers = async (users: User[]) => { + const dateTime = getDateTimeStamp(); + + // Reset counters for each import run + processed = 0; + successful = 0; + failed = 0; + errorCounts.clear(); + + s.start(); + const total = users.length; + s.message(`Migrating users: [0/${total}]`); + + for (const user of users) { + await processUserToClerk(user, total, dateTime); + await cooldown(env.DELAY); + } + s.stop(); + + // Display summary + const summary: ImportSummary = { + totalProcessed: total, + successful: successful, + failed: failed, + errorBreakdown: errorCounts, + }; + displaySummary(summary); +}; diff --git a/src/logger.test.ts b/src/logger.test.ts new file mode 100644 index 0000000..25561a7 --- /dev/null +++ b/src/logger.test.ts @@ -0,0 +1,375 @@ +import { describe, expect, test, beforeEach, afterEach } from "vitest"; +import { errorLogger, validationLogger, importLogger } from "./logger"; +import { readFileSync, existsSync, rmSync } from "node:fs"; + +// Helper to clean up logs directory +const cleanupLogs = () => { + if (existsSync("logs")) { + rmSync("logs", { recursive: true }); + } +}; + +describe("errorLogger", () => { + beforeEach(cleanupLogs); + afterEach(cleanupLogs); + + test("logs a single error to errors.log", () => { + const dateTime = "error-single-test"; + + errorLogger( + { + errors: [ + { + code: "1234", + message: "isolinear chip failed to initialize", + }, + ], + status: "error", + userId: "123", + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log).toHaveLength(1); + expect(log[0]).toEqual({ + type: "User Creation Error", + userId: "123", + status: "error", + error: undefined, // longMessage is undefined + }); + }); + + test("logs error with longMessage", () => { + const dateTime = "error-longmessage-test"; + + errorLogger( + { + errors: [ + { + code: "form_identifier_exists", + message: "Email already exists", + longMessage: "A user with this email address already exists in the system.", + }, + ], + status: "422", + userId: "user_abc123", + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log[0]).toEqual({ + type: "User Creation Error", + userId: "user_abc123", + status: "422", + error: "A user with this email address already exists in the system.", + }); + }); + + test("logs multiple errors from same payload as separate entries", () => { + const dateTime = "error-multiple-test"; + + errorLogger( + { + errors: [ + { + code: "invalid_email", + message: "Invalid email", + longMessage: "The email address format is invalid.", + }, + { + code: "invalid_password", + message: "Invalid password", + longMessage: "Password does not meet requirements.", + }, + ], + status: "400", + userId: "user_xyz", + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log).toHaveLength(2); + expect(log[0].error).toBe("The email address format is invalid."); + expect(log[1].error).toBe("Password does not meet requirements."); + }); + + test("appends to existing log file", () => { + const dateTime = "error-append-test"; + + // First error + errorLogger( + { + errors: [{ code: "err1", message: "First error" }], + status: "400", + userId: "user_1", + }, + dateTime, + ); + + // Second error + errorLogger( + { + errors: [{ code: "err2", message: "Second error" }], + status: "500", + userId: "user_2", + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log).toHaveLength(2); + expect(log[0].userId).toBe("user_1"); + expect(log[1].userId).toBe("user_2"); + }); + + test("handles rate limit error (429)", () => { + const dateTime = "error-ratelimit-test"; + + errorLogger( + { + errors: [ + { + code: "rate_limit_exceeded", + message: "Too many requests", + longMessage: "Rate limit exceeded. Please try again later.", + }, + ], + status: "429", + userId: "user_rate", + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log[0].status).toBe("429"); + expect(log[0].error).toBe("Rate limit exceeded. Please try again later."); + }); +}); + +describe("validationLogger", () => { + beforeEach(cleanupLogs); + afterEach(cleanupLogs); + + test("logs a validation error to errors.log", () => { + const dateTime = "validation-basic-test"; + + validationLogger( + { + error: "invalid_type for required field.", + path: ["email"], + id: "user_123", + row: 5, + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log).toHaveLength(1); + expect(log[0]).toEqual({ + type: "Validation Error", + row: 5, + id: "user_123", + error: "invalid_type for required field.", + path: ["email"], + }); + }); + + test("logs validation error with nested path", () => { + const dateTime = "validation-nested-test"; + + validationLogger( + { + error: "invalid_type for required field.", + path: ["unsafeMetadata", "customField"], + id: "user_456", + row: 10, + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log[0].path).toEqual(["unsafeMetadata", "customField"]); + }); + + test("logs validation error with numeric path (array index)", () => { + const dateTime = "validation-array-test"; + + validationLogger( + { + error: "invalid_email for required field.", + path: ["email", 1], + id: "user_789", + row: 3, + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log[0].path).toEqual(["email", 1]); + }); + + test("appends multiple validation errors", () => { + const dateTime = "validation-append-test"; + + validationLogger( + { + error: "missing userId", + path: ["userId"], + id: "unknown", + row: 1, + }, + dateTime, + ); + + validationLogger( + { + error: "invalid email format", + path: ["email"], + id: "user_2", + row: 2, + }, + dateTime, + ); + + validationLogger( + { + error: "invalid passwordHasher", + path: ["passwordHasher"], + id: "user_3", + row: 3, + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log).toHaveLength(3); + expect(log[0].row).toBe(1); + expect(log[1].row).toBe(2); + expect(log[2].row).toBe(3); + }); +}); + +describe("importLogger", () => { + beforeEach(cleanupLogs); + afterEach(cleanupLogs); + + test("logs a successful import", () => { + const dateTime = "import-success-test"; + + importLogger( + { userId: "user_123", status: "success" }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-import.log`, "utf8")); + expect(log).toHaveLength(1); + expect(log[0]).toEqual({ + userId: "user_123", + status: "success", + }); + }); + + test("logs a failed import with error", () => { + const dateTime = "import-error-test"; + + importLogger( + { userId: "user_456", status: "error", error: "Email already exists" }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-import.log`, "utf8")); + expect(log).toHaveLength(1); + expect(log[0]).toEqual({ + userId: "user_456", + status: "error", + error: "Email already exists", + }); + }); + + test("logs multiple imports in sequence", () => { + const dateTime = "import-multiple-test"; + + importLogger({ userId: "user_1", status: "success" }, dateTime); + importLogger({ userId: "user_2", status: "error", error: "Invalid email" }, dateTime); + importLogger({ userId: "user_3", status: "success" }, dateTime); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-import.log`, "utf8")); + expect(log).toHaveLength(3); + expect(log[0].userId).toBe("user_1"); + expect(log[0].status).toBe("success"); + expect(log[1].userId).toBe("user_2"); + expect(log[1].status).toBe("error"); + expect(log[1].error).toBe("Invalid email"); + expect(log[2].userId).toBe("user_3"); + expect(log[2].status).toBe("success"); + }); +}); + +describe("mixed logging", () => { + beforeEach(cleanupLogs); + afterEach(cleanupLogs); + + test("error and validation logs go to same errors.log file", () => { + const dateTime = "mixed-errors-test"; + + errorLogger( + { + errors: [{ code: "err", message: "API error" }], + status: "500", + userId: "user_1", + }, + dateTime, + ); + + validationLogger( + { + error: "validation failed", + path: ["email"], + id: "user_2", + row: 5, + }, + dateTime, + ); + + const log = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + expect(log).toHaveLength(2); + expect(log[0].type).toBe("User Creation Error"); + expect(log[1].type).toBe("Validation Error"); + }); + + test("error logs and import logs go to separate files", () => { + const dateTime = "mixed-separate-test"; + + errorLogger( + { + errors: [{ code: "err", message: "API error", longMessage: "API error occurred" }], + status: "500", + userId: "user_1", + }, + dateTime, + ); + + importLogger( + { userId: "user_1", status: "error", error: "API error occurred" }, + dateTime, + ); + + importLogger( + { userId: "user_2", status: "success" }, + dateTime, + ); + + const errorLog = JSON.parse(readFileSync(`logs/${dateTime}-errors.log`, "utf8")); + const importLog = JSON.parse(readFileSync(`logs/${dateTime}-import.log`, "utf8")); + + expect(errorLog).toHaveLength(1); + expect(errorLog[0].type).toBe("User Creation Error"); + + expect(importLog).toHaveLength(2); + expect(importLog[0].status).toBe("error"); + expect(importLog[1].status).toBe("success"); + }); +}); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..a473621 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,68 @@ +import fs from "fs"; +import path from "path"; +import { + ErrorLog, + ErrorPayload, + ImportLogEntry, + ValidationErrorPayload, +} from "./types"; + +const confirmOrCreateFolder = (folderPath: string) => { + try { + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath); + } + } catch (err) { + console.error("Error creating directory for logs:", err); + } +}; + +const getLogPath = () => path.join(__dirname, "..", "logs"); + +function appendToLogFile(filePath: string, entry: unknown) { + try { + const logPath = getLogPath(); + confirmOrCreateFolder(logPath); + const fullPath = `${logPath}/${filePath}`; + + if (!fs.existsSync(fullPath)) { + fs.writeFileSync(fullPath, JSON.stringify([entry], null, 2)); + } else { + const log = JSON.parse(fs.readFileSync(fullPath, "utf-8")); + log.push(entry); + fs.writeFileSync(fullPath, JSON.stringify(log, null, 2)); + } + } catch (err) { + console.error("Error writing to log file:", err); + } +} + +export const errorLogger = (payload: ErrorPayload, dateTime: string) => { + for (const err of payload.errors) { + const errorToLog: ErrorLog = { + type: "User Creation Error", + userId: payload.userId, + status: payload.status, + error: err.longMessage, + }; + appendToLogFile(`${dateTime}-errors.log`, errorToLog); + } +}; + +export const validationLogger = ( + payload: ValidationErrorPayload, + dateTime: string, +) => { + const error = { + type: "Validation Error", + row: payload.row, + id: payload.id, + error: payload.error, + path: payload.path, + }; + appendToLogFile(`${dateTime}-errors.log`, error); +}; + +export const importLogger = (entry: ImportLogEntry, dateTime: string) => { + appendToLogFile(`${dateTime}-import.log`, entry); +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..0cf9305 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,52 @@ +import { ClerkAPIError } from "@clerk/types"; +import { handlers } from "./handlers"; +import { userSchema } from "./validators"; +import * as z from "zod"; + +export type User = z.infer; + +// emulate what Clack CLI expects for an option in a Select / MultiSelect +export type OptionType = { + value: string; + label: string | undefined; + hint?: string | undefined; +}; + +// create union of string literals from handlers transformer object keys +export type HandlerMapKeys = (typeof handlers)[number]["key"]; + +// create a union of all transformer objects in handlers array +export type HandlerMapUnion = (typeof handlers)[number]; + +export type ErrorPayload = { + userId: string; + status: string; + errors: ClerkAPIError[]; +}; + +export type ValidationErrorPayload = { + error: string; + path: (string | number)[]; + id: string; + row: number; +}; + +export type ErrorLog = { + type: string; + userId: string; + status: string; + error: string | undefined; +}; + +export type ImportLogEntry = { + userId: string; + status: "success" | "error"; + error?: string; +}; + +export type ImportSummary = { + totalProcessed: number; + successful: number; + failed: number; + errorBreakdown: Map; +}; diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..46ddd69 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, test, vi } from "vitest"; +import { + cooldown, + getDateTimeStamp, + createImportFilePath, + checkIfFileExists, + getFileType, + tryCatch, +} from "./utils"; +import path from "path"; + +describe("cooldown", () => { + test("waits for specified milliseconds", async () => { + const start = Date.now(); + await cooldown(50); + const elapsed = Date.now() - start; + expect(elapsed).toBeGreaterThanOrEqual(45); // allow small variance + expect(elapsed).toBeLessThan(100); + }); + + test("resolves with undefined", async () => { + const result = await cooldown(1); + expect(result).toBeUndefined(); + }); +}); + +describe("getDateTimeStamp", () => { + test("returns ISO format without milliseconds", () => { + const result = getDateTimeStamp(); + // Format: YYYY-MM-DDTHH:mm:ss + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/); + }); + + test("does not include milliseconds or timezone", () => { + const result = getDateTimeStamp(); + expect(result).not.toContain("."); + expect(result).not.toContain("Z"); + }); + + test("returns current time (within 1 second)", () => { + const result = getDateTimeStamp(); + const now = new Date().toISOString().split(".")[0]; + // Compare date portion at minimum + expect(result.substring(0, 10)).toBe(now.substring(0, 10)); + }); +}); + +describe("createImportFilePath", () => { + test("creates path relative to project root", () => { + const result = createImportFilePath("/samples/test.json"); + expect(result).toContain("samples"); + expect(result).toContain("test.json"); + expect(path.isAbsolute(result)).toBe(true); + }); + + test("handles file without leading slash", () => { + const result = createImportFilePath("users.json"); + expect(result).toContain("users.json"); + expect(path.isAbsolute(result)).toBe(true); + }); +}); + +describe("checkIfFileExists", () => { + test("returns true for existing file", () => { + const result = checkIfFileExists("/samples/clerk.json"); + expect(result).toBe(true); + }); + + test("returns false for non-existent file", () => { + const result = checkIfFileExists("/samples/does-not-exist.json"); + expect(result).toBe(false); + }); + + test("returns false for non-existent directory", () => { + const result = checkIfFileExists("/fake-dir/fake-file.json"); + expect(result).toBe(false); + }); +}); + +describe("getFileType", () => { + test("returns application/json for .json files", () => { + const result = getFileType("/samples/clerk.json"); + expect(result).toBe("application/json"); + }); + + test("returns text/csv for .csv files", () => { + // Create path that would be a CSV + const result = getFileType("/samples/test.csv"); + expect(result).toBe("text/csv"); + }); + + test("returns false for unknown file types", () => { + const result = getFileType("/samples/test.xyz123"); + expect(result).toBe(false); + }); +}); + +describe("tryCatch", () => { + test("returns [data, null] on successful promise", async () => { + const promise = Promise.resolve("success"); + const [data, error] = await tryCatch(promise); + expect(data).toBe("success"); + expect(error).toBeNull(); + }); + + test("returns [null, error] on rejected promise with Error", async () => { + const promise = Promise.reject(new Error("test error")); + const [data, error] = await tryCatch(promise); + expect(data).toBeNull(); + expect(error).toBeInstanceOf(Error); + expect(error?.message).toBe("test error"); + }); + + test("throws non-Error throwables", async () => { + const promise = Promise.reject("string error"); + await expect(tryCatch(promise)).rejects.toBe("string error"); + }); + + test("works with async functions", async () => { + const asyncFn = async () => { + await cooldown(1); + return { id: 1, name: "test" }; + }; + const [data, error] = await tryCatch(asyncFn()); + expect(data).toEqual({ id: 1, name: "test" }); + expect(error).toBeNull(); + }); + + test("handles async function errors", async () => { + const asyncFn = async () => { + await cooldown(1); + throw new Error("async error"); + }; + const [data, error] = await tryCatch(asyncFn()); + expect(data).toBeNull(); + expect(error?.message).toBe("async error"); + }); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..593e18c --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,44 @@ +import path from "path"; +import mime from "mime-types"; +import fs from "fs"; + +export async function cooldown(ms: number) { + await new Promise((r) => setTimeout(r, ms)); +} + +export const getDateTimeStamp = () => { + return new Date().toISOString().split(".")[0]; // YYYY-MM-DDTHH:mm:ss +}; + +// utility function to create file path +export const createImportFilePath = (file: string) => { + return path.join(__dirname, "..", file); +}; + +// make sure the file exists. CLI will error if it doesn't +export const checkIfFileExists = (file: string) => { + if (fs.existsSync(createImportFilePath(file))) { + return true; + } else { + return false; + } +}; + +// get the file type so we can verify if this is a JSON or CSV +export const getFileType = (file: string) => { + return mime.lookup(createImportFilePath(file)); +}; + +// awaitable wrapper that returns 'data' and 'error' +export const tryCatch = async ( + promise: Promise, +): Promise<[T, null] | [null, Error]> => { + try { + const data = await promise; + return [data, null]; + } catch (throwable) { + if (throwable instanceof Error) return [null, throwable]; + + throw throwable; + } +}; diff --git a/src/validators.test.ts b/src/validators.test.ts new file mode 100644 index 0000000..d796e20 --- /dev/null +++ b/src/validators.test.ts @@ -0,0 +1,241 @@ +import { describe, expect, test } from "vitest"; +import { userSchema } from "./validators"; + +describe("userSchema", () => { + describe("userId (required)", () => { + test("passes with userId and email", () => { + const result = userSchema.safeParse({ userId: "user_123", email: "test@example.com" }); + expect(result.success).toBe(true); + }); + + test("passes with userId and phone", () => { + const result = userSchema.safeParse({ userId: "user_123", phone: "+1234567890" }); + expect(result.success).toBe(true); + }); + + test("fails when userId is missing", () => { + const result = userSchema.safeParse({ email: "test@example.com" }); + expect(result.success).toBe(false); + }); + + test("fails with only userId (no email or phone)", () => { + const result = userSchema.safeParse({ userId: "user_123" }); + expect(result.success).toBe(false); + }); + }); + + describe("email or phone requirement", () => { + test("passes with email only", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + }); + expect(result.success).toBe(true); + }); + + test("passes with phone only", () => { + const result = userSchema.safeParse({ + userId: "user_123", + phone: "+1234567890", + }); + expect(result.success).toBe(true); + }); + + test("passes with emailAddresses only", () => { + const result = userSchema.safeParse({ + userId: "user_123", + emailAddresses: "test@example.com", + }); + expect(result.success).toBe(true); + }); + + test("passes with phoneNumbers only", () => { + const result = userSchema.safeParse({ + userId: "user_123", + phoneNumbers: "+1234567890", + }); + expect(result.success).toBe(true); + }); + + test("fails without email or phone", () => { + const result = userSchema.safeParse({ + userId: "user_123", + firstName: "John", + }); + expect(result.success).toBe(false); + }); + }); + + describe("email field", () => { + test("passes with email as string", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + }); + expect(result.success).toBe(true); + }); + + test("passes with email as array", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: ["test@example.com", "other@example.com"], + }); + expect(result.success).toBe(true); + }); + + test("fails with invalid email string", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "not-an-email", + phone: "+1234567890", // need valid contact method + }); + expect(result.success).toBe(false); + }); + + test("fails with invalid email in array", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: ["valid@example.com", "not-an-email"], + phone: "+1234567890", // need valid contact method + }); + expect(result.success).toBe(false); + }); + }); + + describe("passwordHasher enum", () => { + const validHashers = [ + "argon2i", + "argon2id", + "bcrypt", + "md5", + "pbkdf2_sha256", + "pbkdf2_sha256_django", + "pbkdf2_sha1", + "scrypt_firebase", + ]; + + test.each(validHashers)("passes with valid hasher: %s", (hasher) => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + password: "hashed_password", + passwordHasher: hasher, + }); + expect(result.success).toBe(true); + }); + + test("fails with invalid passwordHasher", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + password: "hashed_password", + passwordHasher: "invalid_hasher", + }); + expect(result.success).toBe(false); + }); + + test("fails when password provided without passwordHasher", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + password: "hashed_password", + }); + expect(result.success).toBe(false); + }); + + test("passes without password or passwordHasher (with email)", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + }); + expect(result.success).toBe(true); + }); + }); + + describe("phone fields", () => { + test("passes with phone as array", () => { + const result = userSchema.safeParse({ + userId: "user_123", + phone: ["+1234567890"], + }); + expect(result.success).toBe(true); + }); + + test("passes with phone as string", () => { + const result = userSchema.safeParse({ + userId: "user_123", + phone: "+1234567890", + }); + expect(result.success).toBe(true); + }); + + test("passes with phoneNumbers as array", () => { + const result = userSchema.safeParse({ + userId: "user_123", + phoneNumbers: ["+1234567890", "+0987654321"], + }); + expect(result.success).toBe(true); + }); + + test("passes without phone when email provided", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + }); + expect(result.success).toBe(true); + }); + }); + + describe("boolean fields", () => { + test("passes with mfaEnabled boolean", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + mfaEnabled: true, + }); + expect(result.success).toBe(true); + }); + + test("passes with backupCodesEnabled boolean", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + backupCodesEnabled: false, + }); + expect(result.success).toBe(true); + }); + + test("fails with mfaEnabled as string", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: "test@example.com", + mfaEnabled: "true", + }); + expect(result.success).toBe(false); + }); + }); + + describe("full user object", () => { + test("passes with all valid fields", () => { + const result = userSchema.safeParse({ + userId: "user_123", + email: ["primary@example.com", "secondary@example.com"], + username: "johndoe", + firstName: "John", + lastName: "Doe", + password: "$2a$10$hashedpassword", + passwordHasher: "bcrypt", + phone: ["+1234567890"], + mfaEnabled: true, + totpSecret: "JBSWY3DPEHPK3PXP", + backupCodesEnabled: true, + backupCodes: "code1,code2,code3", + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.userId).toBe("user_123"); + expect(result.data.email).toEqual(["primary@example.com", "secondary@example.com"]); + } + }); + }); +}); diff --git a/src/validators.ts b/src/validators.ts new file mode 100644 index 0000000..1a068f1 --- /dev/null +++ b/src/validators.ts @@ -0,0 +1,84 @@ +import * as z from "zod"; + +const unsafeMetadataSchema = z.object({}); +// username: z.string().optional(), +// isAccessToBeta: z.boolean().optional(), +// }); + +const publicMetadataSchema = z.object({}); + +const privateMetadataSchema = z.object({}); + +// ============================================================================ +// +// ONLY EDIT BELOW THIS IF YOU ARE ADDING A NEW IMPORT SOURCE +// THAT IS NOT YET SUPPORTED +// +// ============================================================================ + +const passwordHasherEnum = z.enum([ + "argon2i", + "argon2id", + "bcrypt", + "md5", + "pbkdf2_sha256", + "pbkdf2_sha256_django", + "pbkdf2_sha1", + "scrypt_firebase", +]); + +// default schema -- incoming data will be transformed to this format +// All fields are optional except: +// - userId is required (for logging purposes) +// - passwordHasher is required when password is provided +// - user must have either a verified email or verified phone number +export const userSchema = z.object({ + userId: z.string(), + // Email fields + email: z.union([z.string().email(), z.array(z.string().email())]).optional(), + emailAddresses: z.union([z.string().email(), z.array(z.string().email())]).optional(), + unverifiedEmailAddresses: z.union([z.string().email(), z.array(z.string().email())]).optional(), + // Phone fields + phone: z.union([z.string(), z.array(z.string())]).optional(), + phoneNumbers: z.union([z.string(), z.array(z.string())]).optional(), + unverifiedPhoneNumbers: z.union([z.string(), z.array(z.string())]).optional(), + // User info + username: z.string().optional(), + firstName: z.string().optional(), + lastName: z.string().optional(), + // Password + password: z.string().optional(), + passwordHasher: passwordHasherEnum.optional(), + // MFA + mfaEnabled: z.boolean().optional(), + totpSecret: z.string().optional(), + backupCodesEnabled: z.boolean().optional(), + backupCodes: z.string().optional(), + // unsafeMetadata: unsafeMetadataSchema, + // publicMetadata: publicMetadataSchema, + // privateMetadata: privateMetadataSchema, +}).refine( + (data) => !data.password || data.passwordHasher, + { + message: "passwordHasher is required when password is provided", + path: ["passwordHasher"], + } +).refine( + (data) => { + // Helper to check if field has value + const hasValue = (field: unknown): boolean => { + if (!field) return false; + if (typeof field === "string") return field.length > 0; + if (Array.isArray(field)) return field.length > 0; + return false; + }; + // Must have either verified email or verified phone + const hasVerifiedEmail = hasValue(data.email) || hasValue(data.emailAddresses); + const hasVerifiedPhone = hasValue(data.phone) || hasValue(data.phoneNumbers); + return hasVerifiedEmail || hasVerifiedPhone; + }, + { + message: "User must have either a verified email or verified phone number", + path: ["email"], + } +); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8fb6f2d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({});