A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.

feat: add web dashboard with auth and owner-based mapping management

jack 20783b55 e491952c

+1533 -75
._README.md

This is a binary file and will not be displayed.

._crosspost

This is a binary file and will not be displayed.

+42 -51
README.md
··· 2 2 3 3 > **Note**: This project is built on top of [**bird**](https://github.com/steipete/bird) by [@steipete](https://github.com/steipete), which provides the core Twitter interaction capabilities. 4 4 5 - A powerful tool to crosspost your Tweets to Bluesky automatically. Now supports **multiple accounts**, **custom PDS (hosting) locations**, and a **user-friendly CLI** for easy management. 5 + A powerful tool to crosspost your Tweets to Bluesky automatically. Now features a **Web Dashboard** for easy management, **Multi-account support** for different owners, and **Custom PDS** hosting support. 6 6 7 7 ## ✨ Features 8 8 9 - - **Multi-Account Support**: Sync Twitter A -> Bluesky A, Twitter B -> Bluesky B, or multiple Twitters to one Bluesky. 10 - - **Interactive CLI**: Manage all your account mappings and credentials without touching code. 11 - - **Custom PDS Support**: Works with `bsky.social` or any independent Bluesky hosting provider. 12 - - **Thread Support**: Maintains your Twitter threads perfectly on Bluesky. 13 - - **Media Support**: Automatically migrates high-quality images and videos. 14 - - **Smart Logic**: Automatically detects languages and expands short links. 15 - - **Safety First**: Includes a `--dry-run` mode to test before you post. 9 + - **Web Dashboard**: Modern interface to manage all your sync tasks. 10 + - **Multi-User Mapping**: Let others add their accounts (e.g., Dan, Josh) with their own owners. 11 + - **Multi-Account Support**: Sync Twitter A -> Bluesky A, Twitter B -> Bluesky B, etc. 12 + - **Tailscale Ready**: Accessible over your local network or VPN. 13 + - **Interactive CLI**: Manage everything from the terminal with `./crosspost`. 14 + - **High Quality**: Supports threads, high-quality images, and videos. 16 15 17 16 --- 18 17 19 - ## 🚀 Quick Start (For Everyone) 18 + ## 🚀 Quick Start 20 19 21 20 ### 1. Prerequisites 22 - - **Node.js** installed on your computer. 23 - - A Twitter account (preferably an alt/burner for the web cookies). 24 - - A Bluesky account and an **App Password** (Settings -> Privacy & Security -> App Passwords). 21 + - **Node.js** installed. 22 + - A Twitter account (burner recommended) for global cookies. 23 + - Bluesky account(s) with **App Passwords**. 25 24 26 25 ### 2. Installation 27 - Open your terminal and run: 28 26 ```bash 29 27 git clone https://github.com/j4ckxyz/tweets-2-bsky.git 30 28 cd tweets-2-bsky 31 29 npm install 30 + npm run build 32 31 ``` 33 32 34 - ### 3. Setup (Using the CLI) 35 - Instead of editing files, use our simple setup command: 33 + ### 3. Start Syncing & Web UI 36 34 ```bash 37 - # 1. Set your Twitter cookies (one set of cookies works for all mappings) 38 - ./crosspost setup-twitter 39 - 40 - # 2. Add your first account mapping 41 - ./crosspost add-mapping 35 + # This starts both the sync daemon AND the web dashboard 36 + npm start 42 37 ``` 43 - *Note: You can find your Twitter `auth_token` and `ct0` in your browser's developer tools under Application -> Cookies.* 38 + By default, the web interface runs at **http://localhost:3000**. If you are using Tailscale, it's accessible at `http://your-tailscale-ip:3000`. 44 39 45 - ### 4. Run the Sync 46 - ```bash 47 - # Build the project 48 - npm run build 49 - 50 - # Start the automatic syncing daemon 51 - npm start 52 - ``` 40 + ### 4. Setup (Web Dashboard) 41 + 1. Open the dashboard in your browser. 42 + 2. **Register** a new account (email/password). 43 + 3. Log in and go to **Global Twitter Config** to enter your cookies. 44 + 4. Use **Add New Mapping** to connect a Twitter handle to a Bluesky account. 53 45 54 46 --- 55 47 56 48 ## 🛠 Advanced Usage 57 49 58 - ### Backfilling Old Tweets 59 - If you want to import your historical tweets for a specific account: 50 + ### Disable Web Interface 51 + If you only want to run the sync daemon without the web UI: 60 52 ```bash 61 - # Get the command from the CLI help 62 - ./crosspost import-history 63 - 64 - # Example: Import the last 10 tweets for a specific user 65 - npm run import -- --username YOUR_TWITTER_HANDLE --limit 10 53 + npm start -- --no-web 66 54 ``` 67 55 68 - ### Testing with Dry Run 69 - See what would be posted without actually posting anything: 56 + ### Command Line Interface (CLI) 57 + You can still manage everything via the terminal: 70 58 ```bash 71 - npm start -- --dry-run 59 + # Set Twitter cookies 60 + ./crosspost setup-twitter 61 + 62 + # Add a mapping 63 + ./crosspost add-mapping 64 + 65 + # List/Remove 66 + ./crosspost list 67 + ./crosspost remove 72 68 ``` 73 69 74 - ### Management Commands 70 + ### Backfilling Old Tweets 75 71 ```bash 76 - ./crosspost list # Show all active mappings 77 - ./crosspost remove # Remove an account mapping 78 - ./crosspost set-interval # Change how often to check for new tweets 72 + # Example: Import the last 20 tweets for a user 73 + npm run import -- --username YOUR_TWITTER_HANDLE --limit 20 79 74 ``` 80 75 81 76 --- 82 77 83 - ## 📝 Configuration Details 84 - 85 - - **Check Interval**: Default is 5 minutes. 86 - - **Database**: Processed tweets are tracked per-account in the `processed/` folder so you never get duplicates. 87 - - **Service URL**: If you use a custom Bluesky host (like `bsky.network`), you can set it during the `add-mapping` process. 78 + ## ⚙️ How to get Twitter Cookies 79 + 1. Log in to Twitter in your browser. 80 + 2. Open **Developer Tools** (F12) -> **Application** tab -> **Cookies**. 81 + 3. Copy `auth_token` and `ct0` values. 88 82 89 83 ## ⚖️ License 90 84 MIT 91 - 92 - --- 93 - *Created with ❤️ for the decentralized web.*
+1010
package-lock.json
··· 12 12 "@atproto/api": "^0.18.9", 13 13 "@steipete/bird": "^0.4.0", 14 14 "axios": "^1.13.2", 15 + "bcryptjs": "^3.0.3", 15 16 "commander": "^14.0.2", 17 + "cors": "^2.8.5", 16 18 "dotenv": "^17.2.3", 19 + "express": "^5.2.1", 17 20 "franc-min": "^6.2.0", 18 21 "inquirer": "^13.1.0", 19 22 "iso-639-1": "^3.1.2", 23 + "jsonwebtoken": "^9.0.3", 20 24 "node-cron": "^4.2.1" 21 25 }, 22 26 "devDependencies": { 23 27 "@biomejs/biome": "^1.9.4", 28 + "@types/bcryptjs": "^2.4.6", 29 + "@types/cors": "^2.8.19", 30 + "@types/express": "^5.0.6", 24 31 "@types/inquirer": "^9.0.9", 32 + "@types/jsonwebtoken": "^9.0.10", 25 33 "@types/node": "^22.10.2", 26 34 "tsx": "^4.19.2", 27 35 "typescript": "^5.7.2" ··· 1056 1064 "node": ">=20" 1057 1065 } 1058 1066 }, 1067 + "node_modules/@types/bcryptjs": { 1068 + "version": "2.4.6", 1069 + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", 1070 + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", 1071 + "dev": true, 1072 + "license": "MIT" 1073 + }, 1074 + "node_modules/@types/body-parser": { 1075 + "version": "1.19.6", 1076 + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", 1077 + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", 1078 + "dev": true, 1079 + "license": "MIT", 1080 + "dependencies": { 1081 + "@types/connect": "*", 1082 + "@types/node": "*" 1083 + } 1084 + }, 1085 + "node_modules/@types/connect": { 1086 + "version": "3.4.38", 1087 + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 1088 + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 1089 + "dev": true, 1090 + "license": "MIT", 1091 + "dependencies": { 1092 + "@types/node": "*" 1093 + } 1094 + }, 1095 + "node_modules/@types/cors": { 1096 + "version": "2.8.19", 1097 + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", 1098 + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", 1099 + "dev": true, 1100 + "license": "MIT", 1101 + "dependencies": { 1102 + "@types/node": "*" 1103 + } 1104 + }, 1105 + "node_modules/@types/express": { 1106 + "version": "5.0.6", 1107 + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", 1108 + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", 1109 + "dev": true, 1110 + "license": "MIT", 1111 + "dependencies": { 1112 + "@types/body-parser": "*", 1113 + "@types/express-serve-static-core": "^5.0.0", 1114 + "@types/serve-static": "^2" 1115 + } 1116 + }, 1117 + "node_modules/@types/express-serve-static-core": { 1118 + "version": "5.1.0", 1119 + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", 1120 + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", 1121 + "dev": true, 1122 + "license": "MIT", 1123 + "dependencies": { 1124 + "@types/node": "*", 1125 + "@types/qs": "*", 1126 + "@types/range-parser": "*", 1127 + "@types/send": "*" 1128 + } 1129 + }, 1130 + "node_modules/@types/http-errors": { 1131 + "version": "2.0.5", 1132 + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", 1133 + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", 1134 + "dev": true, 1135 + "license": "MIT" 1136 + }, 1059 1137 "node_modules/@types/inquirer": { 1060 1138 "version": "9.0.9", 1061 1139 "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.9.tgz", ··· 1067 1145 "rxjs": "^7.2.0" 1068 1146 } 1069 1147 }, 1148 + "node_modules/@types/jsonwebtoken": { 1149 + "version": "9.0.10", 1150 + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", 1151 + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", 1152 + "dev": true, 1153 + "license": "MIT", 1154 + "dependencies": { 1155 + "@types/ms": "*", 1156 + "@types/node": "*" 1157 + } 1158 + }, 1159 + "node_modules/@types/ms": { 1160 + "version": "2.1.0", 1161 + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", 1162 + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", 1163 + "dev": true, 1164 + "license": "MIT" 1165 + }, 1070 1166 "node_modules/@types/node": { 1071 1167 "version": "22.19.3", 1072 1168 "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", ··· 1075 1171 "license": "MIT", 1076 1172 "dependencies": { 1077 1173 "undici-types": "~6.21.0" 1174 + } 1175 + }, 1176 + "node_modules/@types/qs": { 1177 + "version": "6.14.0", 1178 + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", 1179 + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", 1180 + "dev": true, 1181 + "license": "MIT" 1182 + }, 1183 + "node_modules/@types/range-parser": { 1184 + "version": "1.2.7", 1185 + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 1186 + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 1187 + "dev": true, 1188 + "license": "MIT" 1189 + }, 1190 + "node_modules/@types/send": { 1191 + "version": "1.2.1", 1192 + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", 1193 + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", 1194 + "dev": true, 1195 + "license": "MIT", 1196 + "dependencies": { 1197 + "@types/node": "*" 1198 + } 1199 + }, 1200 + "node_modules/@types/serve-static": { 1201 + "version": "2.2.0", 1202 + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", 1203 + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", 1204 + "dev": true, 1205 + "license": "MIT", 1206 + "dependencies": { 1207 + "@types/http-errors": "*", 1208 + "@types/node": "*" 1078 1209 } 1079 1210 }, 1080 1211 "node_modules/@types/through": { ··· 1087 1218 "@types/node": "*" 1088 1219 } 1089 1220 }, 1221 + "node_modules/accepts": { 1222 + "version": "2.0.0", 1223 + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 1224 + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 1225 + "license": "MIT", 1226 + "dependencies": { 1227 + "mime-types": "^3.0.0", 1228 + "negotiator": "^1.0.0" 1229 + }, 1230 + "engines": { 1231 + "node": ">= 0.6" 1232 + } 1233 + }, 1234 + "node_modules/accepts/node_modules/mime-db": { 1235 + "version": "1.54.0", 1236 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 1237 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 1238 + "license": "MIT", 1239 + "engines": { 1240 + "node": ">= 0.6" 1241 + } 1242 + }, 1243 + "node_modules/accepts/node_modules/mime-types": { 1244 + "version": "3.0.2", 1245 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", 1246 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", 1247 + "license": "MIT", 1248 + "dependencies": { 1249 + "mime-db": "^1.54.0" 1250 + }, 1251 + "engines": { 1252 + "node": ">=18" 1253 + }, 1254 + "funding": { 1255 + "type": "opencollective", 1256 + "url": "https://opencollective.com/express" 1257 + } 1258 + }, 1090 1259 "node_modules/ansi-regex": { 1091 1260 "version": "6.2.2", 1092 1261 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", ··· 1134 1303 "proxy-from-env": "^1.1.0" 1135 1304 } 1136 1305 }, 1306 + "node_modules/bcryptjs": { 1307 + "version": "3.0.3", 1308 + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", 1309 + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", 1310 + "license": "BSD-3-Clause", 1311 + "bin": { 1312 + "bcrypt": "bin/bcrypt" 1313 + } 1314 + }, 1315 + "node_modules/body-parser": { 1316 + "version": "2.2.1", 1317 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", 1318 + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", 1319 + "license": "MIT", 1320 + "dependencies": { 1321 + "bytes": "^3.1.2", 1322 + "content-type": "^1.0.5", 1323 + "debug": "^4.4.3", 1324 + "http-errors": "^2.0.0", 1325 + "iconv-lite": "^0.7.0", 1326 + "on-finished": "^2.4.1", 1327 + "qs": "^6.14.0", 1328 + "raw-body": "^3.0.1", 1329 + "type-is": "^2.0.1" 1330 + }, 1331 + "engines": { 1332 + "node": ">=18" 1333 + }, 1334 + "funding": { 1335 + "type": "opencollective", 1336 + "url": "https://opencollective.com/express" 1337 + } 1338 + }, 1339 + "node_modules/buffer-equal-constant-time": { 1340 + "version": "1.0.1", 1341 + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 1342 + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 1343 + "license": "BSD-3-Clause" 1344 + }, 1345 + "node_modules/bytes": { 1346 + "version": "3.1.2", 1347 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 1348 + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 1349 + "license": "MIT", 1350 + "engines": { 1351 + "node": ">= 0.8" 1352 + } 1353 + }, 1137 1354 "node_modules/call-bind-apply-helpers": { 1138 1355 "version": "1.0.2", 1139 1356 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", ··· 1147 1364 "node": ">= 0.4" 1148 1365 } 1149 1366 }, 1367 + "node_modules/call-bound": { 1368 + "version": "1.0.4", 1369 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 1370 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 1371 + "license": "MIT", 1372 + "dependencies": { 1373 + "call-bind-apply-helpers": "^1.0.2", 1374 + "get-intrinsic": "^1.3.0" 1375 + }, 1376 + "engines": { 1377 + "node": ">= 0.4" 1378 + }, 1379 + "funding": { 1380 + "url": "https://github.com/sponsors/ljharb" 1381 + } 1382 + }, 1150 1383 "node_modules/chardet": { 1151 1384 "version": "2.1.1", 1152 1385 "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", ··· 1193 1426 "node": ">=20" 1194 1427 } 1195 1428 }, 1429 + "node_modules/content-disposition": { 1430 + "version": "1.0.1", 1431 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", 1432 + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", 1433 + "license": "MIT", 1434 + "engines": { 1435 + "node": ">=18" 1436 + }, 1437 + "funding": { 1438 + "type": "opencollective", 1439 + "url": "https://opencollective.com/express" 1440 + } 1441 + }, 1442 + "node_modules/content-type": { 1443 + "version": "1.0.5", 1444 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 1445 + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 1446 + "license": "MIT", 1447 + "engines": { 1448 + "node": ">= 0.6" 1449 + } 1450 + }, 1451 + "node_modules/cookie": { 1452 + "version": "0.7.2", 1453 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 1454 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 1455 + "license": "MIT", 1456 + "engines": { 1457 + "node": ">= 0.6" 1458 + } 1459 + }, 1460 + "node_modules/cookie-signature": { 1461 + "version": "1.2.2", 1462 + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 1463 + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 1464 + "license": "MIT", 1465 + "engines": { 1466 + "node": ">=6.6.0" 1467 + } 1468 + }, 1469 + "node_modules/cors": { 1470 + "version": "2.8.5", 1471 + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 1472 + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 1473 + "license": "MIT", 1474 + "dependencies": { 1475 + "object-assign": "^4", 1476 + "vary": "^1" 1477 + }, 1478 + "engines": { 1479 + "node": ">= 0.10" 1480 + } 1481 + }, 1482 + "node_modules/debug": { 1483 + "version": "4.4.3", 1484 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 1485 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 1486 + "license": "MIT", 1487 + "dependencies": { 1488 + "ms": "^2.1.3" 1489 + }, 1490 + "engines": { 1491 + "node": ">=6.0" 1492 + }, 1493 + "peerDependenciesMeta": { 1494 + "supports-color": { 1495 + "optional": true 1496 + } 1497 + } 1498 + }, 1196 1499 "node_modules/delayed-stream": { 1197 1500 "version": "1.0.0", 1198 1501 "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", ··· 1200 1503 "license": "MIT", 1201 1504 "engines": { 1202 1505 "node": ">=0.4.0" 1506 + } 1507 + }, 1508 + "node_modules/depd": { 1509 + "version": "2.0.0", 1510 + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 1511 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 1512 + "license": "MIT", 1513 + "engines": { 1514 + "node": ">= 0.8" 1203 1515 } 1204 1516 }, 1205 1517 "node_modules/dotenv": { ··· 1228 1540 "node": ">= 0.4" 1229 1541 } 1230 1542 }, 1543 + "node_modules/ecdsa-sig-formatter": { 1544 + "version": "1.0.11", 1545 + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 1546 + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 1547 + "license": "Apache-2.0", 1548 + "dependencies": { 1549 + "safe-buffer": "^5.0.1" 1550 + } 1551 + }, 1552 + "node_modules/ee-first": { 1553 + "version": "1.1.1", 1554 + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1555 + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 1556 + "license": "MIT" 1557 + }, 1231 1558 "node_modules/emoji-regex": { 1232 1559 "version": "10.6.0", 1233 1560 "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", 1234 1561 "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", 1235 1562 "license": "MIT" 1563 + }, 1564 + "node_modules/encodeurl": { 1565 + "version": "2.0.0", 1566 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 1567 + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 1568 + "license": "MIT", 1569 + "engines": { 1570 + "node": ">= 0.8" 1571 + } 1236 1572 }, 1237 1573 "node_modules/es-define-property": { 1238 1574 "version": "1.0.1", ··· 1321 1657 "@esbuild/win32-x64": "0.27.2" 1322 1658 } 1323 1659 }, 1660 + "node_modules/escape-html": { 1661 + "version": "1.0.3", 1662 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1663 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 1664 + "license": "MIT" 1665 + }, 1666 + "node_modules/etag": { 1667 + "version": "1.8.1", 1668 + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 1669 + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 1670 + "license": "MIT", 1671 + "engines": { 1672 + "node": ">= 0.6" 1673 + } 1674 + }, 1675 + "node_modules/express": { 1676 + "version": "5.2.1", 1677 + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", 1678 + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", 1679 + "license": "MIT", 1680 + "dependencies": { 1681 + "accepts": "^2.0.0", 1682 + "body-parser": "^2.2.1", 1683 + "content-disposition": "^1.0.0", 1684 + "content-type": "^1.0.5", 1685 + "cookie": "^0.7.1", 1686 + "cookie-signature": "^1.2.1", 1687 + "debug": "^4.4.0", 1688 + "depd": "^2.0.0", 1689 + "encodeurl": "^2.0.0", 1690 + "escape-html": "^1.0.3", 1691 + "etag": "^1.8.1", 1692 + "finalhandler": "^2.1.0", 1693 + "fresh": "^2.0.0", 1694 + "http-errors": "^2.0.0", 1695 + "merge-descriptors": "^2.0.0", 1696 + "mime-types": "^3.0.0", 1697 + "on-finished": "^2.4.1", 1698 + "once": "^1.4.0", 1699 + "parseurl": "^1.3.3", 1700 + "proxy-addr": "^2.0.7", 1701 + "qs": "^6.14.0", 1702 + "range-parser": "^1.2.1", 1703 + "router": "^2.2.0", 1704 + "send": "^1.1.0", 1705 + "serve-static": "^2.2.0", 1706 + "statuses": "^2.0.1", 1707 + "type-is": "^2.0.1", 1708 + "vary": "^1.1.2" 1709 + }, 1710 + "engines": { 1711 + "node": ">= 18" 1712 + }, 1713 + "funding": { 1714 + "type": "opencollective", 1715 + "url": "https://opencollective.com/express" 1716 + } 1717 + }, 1718 + "node_modules/express/node_modules/mime-db": { 1719 + "version": "1.54.0", 1720 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 1721 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 1722 + "license": "MIT", 1723 + "engines": { 1724 + "node": ">= 0.6" 1725 + } 1726 + }, 1727 + "node_modules/express/node_modules/mime-types": { 1728 + "version": "3.0.2", 1729 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", 1730 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", 1731 + "license": "MIT", 1732 + "dependencies": { 1733 + "mime-db": "^1.54.0" 1734 + }, 1735 + "engines": { 1736 + "node": ">=18" 1737 + }, 1738 + "funding": { 1739 + "type": "opencollective", 1740 + "url": "https://opencollective.com/express" 1741 + } 1742 + }, 1743 + "node_modules/finalhandler": { 1744 + "version": "2.1.1", 1745 + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", 1746 + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", 1747 + "license": "MIT", 1748 + "dependencies": { 1749 + "debug": "^4.4.0", 1750 + "encodeurl": "^2.0.0", 1751 + "escape-html": "^1.0.3", 1752 + "on-finished": "^2.4.1", 1753 + "parseurl": "^1.3.3", 1754 + "statuses": "^2.0.1" 1755 + }, 1756 + "engines": { 1757 + "node": ">= 18.0.0" 1758 + }, 1759 + "funding": { 1760 + "type": "opencollective", 1761 + "url": "https://opencollective.com/express" 1762 + } 1763 + }, 1324 1764 "node_modules/follow-redirects": { 1325 1765 "version": "1.15.11", 1326 1766 "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", ··· 1357 1797 "node": ">= 6" 1358 1798 } 1359 1799 }, 1800 + "node_modules/forwarded": { 1801 + "version": "0.2.0", 1802 + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 1803 + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 1804 + "license": "MIT", 1805 + "engines": { 1806 + "node": ">= 0.6" 1807 + } 1808 + }, 1360 1809 "node_modules/franc-min": { 1361 1810 "version": "6.2.0", 1362 1811 "resolved": "https://registry.npmjs.org/franc-min/-/franc-min-6.2.0.tgz", ··· 1368 1817 "funding": { 1369 1818 "type": "github", 1370 1819 "url": "https://github.com/sponsors/wooorm" 1820 + } 1821 + }, 1822 + "node_modules/fresh": { 1823 + "version": "2.0.0", 1824 + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 1825 + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 1826 + "license": "MIT", 1827 + "engines": { 1828 + "node": ">= 0.8" 1371 1829 } 1372 1830 }, 1373 1831 "node_modules/fsevents": { ··· 1507 1965 "node": ">= 0.4" 1508 1966 } 1509 1967 }, 1968 + "node_modules/http-errors": { 1969 + "version": "2.0.1", 1970 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", 1971 + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", 1972 + "license": "MIT", 1973 + "dependencies": { 1974 + "depd": "~2.0.0", 1975 + "inherits": "~2.0.4", 1976 + "setprototypeof": "~1.2.0", 1977 + "statuses": "~2.0.2", 1978 + "toidentifier": "~1.0.1" 1979 + }, 1980 + "engines": { 1981 + "node": ">= 0.8" 1982 + }, 1983 + "funding": { 1984 + "type": "opencollective", 1985 + "url": "https://opencollective.com/express" 1986 + } 1987 + }, 1510 1988 "node_modules/iconv-lite": { 1511 1989 "version": "0.7.1", 1512 1990 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", ··· 1523 2001 "url": "https://opencollective.com/express" 1524 2002 } 1525 2003 }, 2004 + "node_modules/inherits": { 2005 + "version": "2.0.4", 2006 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 2007 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 2008 + "license": "ISC" 2009 + }, 1526 2010 "node_modules/inquirer": { 1527 2011 "version": "13.1.0", 1528 2012 "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.1.0.tgz", ··· 1549 2033 } 1550 2034 } 1551 2035 }, 2036 + "node_modules/ipaddr.js": { 2037 + "version": "1.9.1", 2038 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 2039 + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 2040 + "license": "MIT", 2041 + "engines": { 2042 + "node": ">= 0.10" 2043 + } 2044 + }, 2045 + "node_modules/is-promise": { 2046 + "version": "4.0.0", 2047 + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 2048 + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 2049 + "license": "MIT" 2050 + }, 1552 2051 "node_modules/iso-639-1": { 1553 2052 "version": "3.1.5", 1554 2053 "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", ··· 1576 2075 "node": ">=6" 1577 2076 } 1578 2077 }, 2078 + "node_modules/jsonwebtoken": { 2079 + "version": "9.0.3", 2080 + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", 2081 + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", 2082 + "license": "MIT", 2083 + "dependencies": { 2084 + "jws": "^4.0.1", 2085 + "lodash.includes": "^4.3.0", 2086 + "lodash.isboolean": "^3.0.3", 2087 + "lodash.isinteger": "^4.0.4", 2088 + "lodash.isnumber": "^3.0.3", 2089 + "lodash.isplainobject": "^4.0.6", 2090 + "lodash.isstring": "^4.0.1", 2091 + "lodash.once": "^4.0.0", 2092 + "ms": "^2.1.1", 2093 + "semver": "^7.5.4" 2094 + }, 2095 + "engines": { 2096 + "node": ">=12", 2097 + "npm": ">=6" 2098 + } 2099 + }, 2100 + "node_modules/jwa": { 2101 + "version": "2.0.1", 2102 + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", 2103 + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", 2104 + "license": "MIT", 2105 + "dependencies": { 2106 + "buffer-equal-constant-time": "^1.0.1", 2107 + "ecdsa-sig-formatter": "1.0.11", 2108 + "safe-buffer": "^5.0.1" 2109 + } 2110 + }, 2111 + "node_modules/jws": { 2112 + "version": "4.0.1", 2113 + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", 2114 + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", 2115 + "license": "MIT", 2116 + "dependencies": { 2117 + "jwa": "^2.0.1", 2118 + "safe-buffer": "^5.0.1" 2119 + } 2120 + }, 1579 2121 "node_modules/kleur": { 1580 2122 "version": "4.1.5", 1581 2123 "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", ··· 1585 2127 "node": ">=6" 1586 2128 } 1587 2129 }, 2130 + "node_modules/lodash.includes": { 2131 + "version": "4.3.0", 2132 + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 2133 + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", 2134 + "license": "MIT" 2135 + }, 2136 + "node_modules/lodash.isboolean": { 2137 + "version": "3.0.3", 2138 + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 2139 + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", 2140 + "license": "MIT" 2141 + }, 2142 + "node_modules/lodash.isinteger": { 2143 + "version": "4.0.4", 2144 + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 2145 + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", 2146 + "license": "MIT" 2147 + }, 2148 + "node_modules/lodash.isnumber": { 2149 + "version": "3.0.3", 2150 + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 2151 + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", 2152 + "license": "MIT" 2153 + }, 2154 + "node_modules/lodash.isplainobject": { 2155 + "version": "4.0.6", 2156 + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 2157 + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 2158 + "license": "MIT" 2159 + }, 2160 + "node_modules/lodash.isstring": { 2161 + "version": "4.0.1", 2162 + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 2163 + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", 2164 + "license": "MIT" 2165 + }, 2166 + "node_modules/lodash.once": { 2167 + "version": "4.1.1", 2168 + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 2169 + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", 2170 + "license": "MIT" 2171 + }, 1588 2172 "node_modules/math-intrinsics": { 1589 2173 "version": "1.1.0", 1590 2174 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", ··· 1594 2178 "node": ">= 0.4" 1595 2179 } 1596 2180 }, 2181 + "node_modules/media-typer": { 2182 + "version": "1.1.0", 2183 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 2184 + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 2185 + "license": "MIT", 2186 + "engines": { 2187 + "node": ">= 0.8" 2188 + } 2189 + }, 2190 + "node_modules/merge-descriptors": { 2191 + "version": "2.0.0", 2192 + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 2193 + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 2194 + "license": "MIT", 2195 + "engines": { 2196 + "node": ">=18" 2197 + }, 2198 + "funding": { 2199 + "url": "https://github.com/sponsors/sindresorhus" 2200 + } 2201 + }, 1597 2202 "node_modules/mime-db": { 1598 2203 "version": "1.52.0", 1599 2204 "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", ··· 1615 2220 "node": ">= 0.6" 1616 2221 } 1617 2222 }, 2223 + "node_modules/ms": { 2224 + "version": "2.1.3", 2225 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 2226 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 2227 + "license": "MIT" 2228 + }, 1618 2229 "node_modules/multiformats": { 1619 2230 "version": "9.9.0", 1620 2231 "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", ··· 1640 2251 "url": "https://github.com/sponsors/wooorm" 1641 2252 } 1642 2253 }, 2254 + "node_modules/negotiator": { 2255 + "version": "1.0.0", 2256 + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 2257 + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 2258 + "license": "MIT", 2259 + "engines": { 2260 + "node": ">= 0.6" 2261 + } 2262 + }, 1643 2263 "node_modules/node-cron": { 1644 2264 "version": "4.2.1", 1645 2265 "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", ··· 1649 2269 "node": ">=6.0.0" 1650 2270 } 1651 2271 }, 2272 + "node_modules/object-assign": { 2273 + "version": "4.1.1", 2274 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 2275 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 2276 + "license": "MIT", 2277 + "engines": { 2278 + "node": ">=0.10.0" 2279 + } 2280 + }, 2281 + "node_modules/object-inspect": { 2282 + "version": "1.13.4", 2283 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 2284 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 2285 + "license": "MIT", 2286 + "engines": { 2287 + "node": ">= 0.4" 2288 + }, 2289 + "funding": { 2290 + "url": "https://github.com/sponsors/ljharb" 2291 + } 2292 + }, 2293 + "node_modules/on-finished": { 2294 + "version": "2.4.1", 2295 + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 2296 + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 2297 + "license": "MIT", 2298 + "dependencies": { 2299 + "ee-first": "1.1.1" 2300 + }, 2301 + "engines": { 2302 + "node": ">= 0.8" 2303 + } 2304 + }, 2305 + "node_modules/once": { 2306 + "version": "1.4.0", 2307 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2308 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 2309 + "license": "ISC", 2310 + "dependencies": { 2311 + "wrappy": "1" 2312 + } 2313 + }, 2314 + "node_modules/parseurl": { 2315 + "version": "1.3.3", 2316 + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 2317 + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 2318 + "license": "MIT", 2319 + "engines": { 2320 + "node": ">= 0.8" 2321 + } 2322 + }, 2323 + "node_modules/path-to-regexp": { 2324 + "version": "8.3.0", 2325 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", 2326 + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", 2327 + "license": "MIT", 2328 + "funding": { 2329 + "type": "opencollective", 2330 + "url": "https://opencollective.com/express" 2331 + } 2332 + }, 2333 + "node_modules/proxy-addr": { 2334 + "version": "2.0.7", 2335 + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 2336 + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 2337 + "license": "MIT", 2338 + "dependencies": { 2339 + "forwarded": "0.2.0", 2340 + "ipaddr.js": "1.9.1" 2341 + }, 2342 + "engines": { 2343 + "node": ">= 0.10" 2344 + } 2345 + }, 1652 2346 "node_modules/proxy-from-env": { 1653 2347 "version": "1.1.0", 1654 2348 "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 1655 2349 "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 1656 2350 "license": "MIT" 1657 2351 }, 2352 + "node_modules/qs": { 2353 + "version": "6.14.1", 2354 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", 2355 + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", 2356 + "license": "BSD-3-Clause", 2357 + "dependencies": { 2358 + "side-channel": "^1.1.0" 2359 + }, 2360 + "engines": { 2361 + "node": ">=0.6" 2362 + }, 2363 + "funding": { 2364 + "url": "https://github.com/sponsors/ljharb" 2365 + } 2366 + }, 2367 + "node_modules/range-parser": { 2368 + "version": "1.2.1", 2369 + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 2370 + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 2371 + "license": "MIT", 2372 + "engines": { 2373 + "node": ">= 0.6" 2374 + } 2375 + }, 2376 + "node_modules/raw-body": { 2377 + "version": "3.0.2", 2378 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", 2379 + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", 2380 + "license": "MIT", 2381 + "dependencies": { 2382 + "bytes": "~3.1.2", 2383 + "http-errors": "~2.0.1", 2384 + "iconv-lite": "~0.7.0", 2385 + "unpipe": "~1.0.0" 2386 + }, 2387 + "engines": { 2388 + "node": ">= 0.10" 2389 + } 2390 + }, 1658 2391 "node_modules/resolve-pkg-maps": { 1659 2392 "version": "1.0.0", 1660 2393 "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", ··· 1665 2398 "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 1666 2399 } 1667 2400 }, 2401 + "node_modules/router": { 2402 + "version": "2.2.0", 2403 + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 2404 + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 2405 + "license": "MIT", 2406 + "dependencies": { 2407 + "debug": "^4.4.0", 2408 + "depd": "^2.0.0", 2409 + "is-promise": "^4.0.0", 2410 + "parseurl": "^1.3.3", 2411 + "path-to-regexp": "^8.0.0" 2412 + }, 2413 + "engines": { 2414 + "node": ">= 18" 2415 + } 2416 + }, 1668 2417 "node_modules/run-async": { 1669 2418 "version": "4.0.6", 1670 2419 "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", ··· 1683 2432 "tslib": "^2.1.0" 1684 2433 } 1685 2434 }, 2435 + "node_modules/safe-buffer": { 2436 + "version": "5.2.1", 2437 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 2438 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 2439 + "funding": [ 2440 + { 2441 + "type": "github", 2442 + "url": "https://github.com/sponsors/feross" 2443 + }, 2444 + { 2445 + "type": "patreon", 2446 + "url": "https://www.patreon.com/feross" 2447 + }, 2448 + { 2449 + "type": "consulting", 2450 + "url": "https://feross.org/support" 2451 + } 2452 + ], 2453 + "license": "MIT" 2454 + }, 1686 2455 "node_modules/safer-buffer": { 1687 2456 "version": "2.1.2", 1688 2457 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1689 2458 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1690 2459 "license": "MIT" 1691 2460 }, 2461 + "node_modules/semver": { 2462 + "version": "7.7.3", 2463 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 2464 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 2465 + "license": "ISC", 2466 + "bin": { 2467 + "semver": "bin/semver.js" 2468 + }, 2469 + "engines": { 2470 + "node": ">=10" 2471 + } 2472 + }, 2473 + "node_modules/send": { 2474 + "version": "1.2.1", 2475 + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", 2476 + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", 2477 + "license": "MIT", 2478 + "dependencies": { 2479 + "debug": "^4.4.3", 2480 + "encodeurl": "^2.0.0", 2481 + "escape-html": "^1.0.3", 2482 + "etag": "^1.8.1", 2483 + "fresh": "^2.0.0", 2484 + "http-errors": "^2.0.1", 2485 + "mime-types": "^3.0.2", 2486 + "ms": "^2.1.3", 2487 + "on-finished": "^2.4.1", 2488 + "range-parser": "^1.2.1", 2489 + "statuses": "^2.0.2" 2490 + }, 2491 + "engines": { 2492 + "node": ">= 18" 2493 + }, 2494 + "funding": { 2495 + "type": "opencollective", 2496 + "url": "https://opencollective.com/express" 2497 + } 2498 + }, 2499 + "node_modules/send/node_modules/mime-db": { 2500 + "version": "1.54.0", 2501 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 2502 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 2503 + "license": "MIT", 2504 + "engines": { 2505 + "node": ">= 0.6" 2506 + } 2507 + }, 2508 + "node_modules/send/node_modules/mime-types": { 2509 + "version": "3.0.2", 2510 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", 2511 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", 2512 + "license": "MIT", 2513 + "dependencies": { 2514 + "mime-db": "^1.54.0" 2515 + }, 2516 + "engines": { 2517 + "node": ">=18" 2518 + }, 2519 + "funding": { 2520 + "type": "opencollective", 2521 + "url": "https://opencollective.com/express" 2522 + } 2523 + }, 2524 + "node_modules/serve-static": { 2525 + "version": "2.2.1", 2526 + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", 2527 + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", 2528 + "license": "MIT", 2529 + "dependencies": { 2530 + "encodeurl": "^2.0.0", 2531 + "escape-html": "^1.0.3", 2532 + "parseurl": "^1.3.3", 2533 + "send": "^1.2.0" 2534 + }, 2535 + "engines": { 2536 + "node": ">= 18" 2537 + }, 2538 + "funding": { 2539 + "type": "opencollective", 2540 + "url": "https://opencollective.com/express" 2541 + } 2542 + }, 2543 + "node_modules/setprototypeof": { 2544 + "version": "1.2.0", 2545 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 2546 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 2547 + "license": "ISC" 2548 + }, 2549 + "node_modules/side-channel": { 2550 + "version": "1.1.0", 2551 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 2552 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 2553 + "license": "MIT", 2554 + "dependencies": { 2555 + "es-errors": "^1.3.0", 2556 + "object-inspect": "^1.13.3", 2557 + "side-channel-list": "^1.0.0", 2558 + "side-channel-map": "^1.0.1", 2559 + "side-channel-weakmap": "^1.0.2" 2560 + }, 2561 + "engines": { 2562 + "node": ">= 0.4" 2563 + }, 2564 + "funding": { 2565 + "url": "https://github.com/sponsors/ljharb" 2566 + } 2567 + }, 2568 + "node_modules/side-channel-list": { 2569 + "version": "1.0.0", 2570 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 2571 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 2572 + "license": "MIT", 2573 + "dependencies": { 2574 + "es-errors": "^1.3.0", 2575 + "object-inspect": "^1.13.3" 2576 + }, 2577 + "engines": { 2578 + "node": ">= 0.4" 2579 + }, 2580 + "funding": { 2581 + "url": "https://github.com/sponsors/ljharb" 2582 + } 2583 + }, 2584 + "node_modules/side-channel-map": { 2585 + "version": "1.0.1", 2586 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 2587 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 2588 + "license": "MIT", 2589 + "dependencies": { 2590 + "call-bound": "^1.0.2", 2591 + "es-errors": "^1.3.0", 2592 + "get-intrinsic": "^1.2.5", 2593 + "object-inspect": "^1.13.3" 2594 + }, 2595 + "engines": { 2596 + "node": ">= 0.4" 2597 + }, 2598 + "funding": { 2599 + "url": "https://github.com/sponsors/ljharb" 2600 + } 2601 + }, 2602 + "node_modules/side-channel-weakmap": { 2603 + "version": "1.0.2", 2604 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 2605 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 2606 + "license": "MIT", 2607 + "dependencies": { 2608 + "call-bound": "^1.0.2", 2609 + "es-errors": "^1.3.0", 2610 + "get-intrinsic": "^1.2.5", 2611 + "object-inspect": "^1.13.3", 2612 + "side-channel-map": "^1.0.1" 2613 + }, 2614 + "engines": { 2615 + "node": ">= 0.4" 2616 + }, 2617 + "funding": { 2618 + "url": "https://github.com/sponsors/ljharb" 2619 + } 2620 + }, 1692 2621 "node_modules/signal-exit": { 1693 2622 "version": "4.1.0", 1694 2623 "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", ··· 1699 2628 }, 1700 2629 "funding": { 1701 2630 "url": "https://github.com/sponsors/isaacs" 2631 + } 2632 + }, 2633 + "node_modules/statuses": { 2634 + "version": "2.0.2", 2635 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", 2636 + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", 2637 + "license": "MIT", 2638 + "engines": { 2639 + "node": ">= 0.8" 1702 2640 } 1703 2641 }, 1704 2642 "node_modules/string-width": { ··· 1740 2678 "license": "MIT", 1741 2679 "bin": { 1742 2680 "tlds": "bin.js" 2681 + } 2682 + }, 2683 + "node_modules/toidentifier": { 2684 + "version": "1.0.1", 2685 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 2686 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 2687 + "license": "MIT", 2688 + "engines": { 2689 + "node": ">=0.6" 1743 2690 } 1744 2691 }, 1745 2692 "node_modules/trigram-utils": { ··· 1782 2729 "fsevents": "~2.3.3" 1783 2730 } 1784 2731 }, 2732 + "node_modules/type-is": { 2733 + "version": "2.0.1", 2734 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 2735 + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 2736 + "license": "MIT", 2737 + "dependencies": { 2738 + "content-type": "^1.0.5", 2739 + "media-typer": "^1.1.0", 2740 + "mime-types": "^3.0.0" 2741 + }, 2742 + "engines": { 2743 + "node": ">= 0.6" 2744 + } 2745 + }, 2746 + "node_modules/type-is/node_modules/mime-db": { 2747 + "version": "1.54.0", 2748 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 2749 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 2750 + "license": "MIT", 2751 + "engines": { 2752 + "node": ">= 0.6" 2753 + } 2754 + }, 2755 + "node_modules/type-is/node_modules/mime-types": { 2756 + "version": "3.0.2", 2757 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", 2758 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", 2759 + "license": "MIT", 2760 + "dependencies": { 2761 + "mime-db": "^1.54.0" 2762 + }, 2763 + "engines": { 2764 + "node": ">=18" 2765 + }, 2766 + "funding": { 2767 + "type": "opencollective", 2768 + "url": "https://opencollective.com/express" 2769 + } 2770 + }, 1785 2771 "node_modules/typescript": { 1786 2772 "version": "5.9.3", 1787 2773 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", ··· 1818 2804 "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 1819 2805 "license": "MIT" 1820 2806 }, 2807 + "node_modules/unpipe": { 2808 + "version": "1.0.0", 2809 + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2810 + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 2811 + "license": "MIT", 2812 + "engines": { 2813 + "node": ">= 0.8" 2814 + } 2815 + }, 2816 + "node_modules/vary": { 2817 + "version": "1.1.2", 2818 + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2819 + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 2820 + "license": "MIT", 2821 + "engines": { 2822 + "node": ">= 0.8" 2823 + } 2824 + }, 1821 2825 "node_modules/wrap-ansi": { 1822 2826 "version": "9.0.2", 1823 2827 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", ··· 1834 2838 "funding": { 1835 2839 "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1836 2840 } 2841 + }, 2842 + "node_modules/wrappy": { 2843 + "version": "1.0.2", 2844 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2845 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2846 + "license": "ISC" 1837 2847 }, 1838 2848 "node_modules/zod": { 1839 2849 "version": "3.25.76",
+8
package.json
··· 28 28 "@atproto/api": "^0.18.9", 29 29 "@steipete/bird": "^0.4.0", 30 30 "axios": "^1.13.2", 31 + "bcryptjs": "^3.0.3", 31 32 "commander": "^14.0.2", 33 + "cors": "^2.8.5", 32 34 "dotenv": "^17.2.3", 35 + "express": "^5.2.1", 33 36 "franc-min": "^6.2.0", 34 37 "inquirer": "^13.1.0", 35 38 "iso-639-1": "^3.1.2", 39 + "jsonwebtoken": "^9.0.3", 36 40 "node-cron": "^4.2.1" 37 41 }, 38 42 "devDependencies": { 39 43 "@biomejs/biome": "^1.9.4", 44 + "@types/bcryptjs": "^2.4.6", 45 + "@types/cors": "^2.8.19", 46 + "@types/express": "^5.0.6", 40 47 "@types/inquirer": "^9.0.9", 48 + "@types/jsonwebtoken": "^9.0.10", 41 49 "@types/node": "^22.10.2", 42 50 "tsx": "^4.19.2", 43 51 "typescript": "^5.7.2"
+279
public/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Tweets-2-Bsky Dashboard</title> 7 + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> 8 + <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> 9 + <style> 10 + body { background-color: #f8f9fa; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } 11 + .card { border: none; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } 12 + .navbar { background-color: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.03); } 13 + .btn-primary { background-color: #0085ff; border: none; border-radius: 8px; } 14 + .btn-primary:hover { background-color: #0072db; } 15 + .login-container { max-width: 400px; margin: 100px auto; } 16 + .owner-badge { background-color: #e9ecef; color: #495057; padding: 4px 8px; border-radius: 6px; font-size: 0.8rem; font-weight: 600; } 17 + </style> 18 + </head> 19 + <body> 20 + <div id="root"></div> 21 + 22 + <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> 23 + <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> 24 + <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> 25 + <script src="https://unpkg.com/axios/dist/axios.min.js"></script> 26 + 27 + <script type="text/babel"> 28 + const { useState, useEffect } = React; 29 + 30 + function App() { 31 + const [token, setToken] = useState(localStorage.getItem('token')); 32 + const [view, setView] = useState('login'); // login, register, dashboard 33 + const [mappings, setMappings] = useState([]); 34 + const [twitterConfig, setTwitterConfig] = useState({ authToken: '', ct0: '' }); 35 + const [loading, setLoading] = useState(false); 36 + const [error, setError] = useState(''); 37 + 38 + useEffect(() => { 39 + if (token) { 40 + fetchData(); 41 + setView('dashboard'); 42 + } 43 + }, [token]); 44 + 45 + const fetchData = async () => { 46 + try { 47 + const headers = { Authorization: `Bearer ${token}` }; 48 + const [mapRes, twitRes] = await Promise.all([ 49 + axios.get('/api/mappings', { headers }), 50 + axios.get('/api/twitter-config', { headers }) 51 + ]); 52 + setMappings(mapRes.data); 53 + setTwitterConfig(twitRes.data); 54 + } catch (err) { 55 + if (err.response?.status === 401) handleLogout(); 56 + } 57 + }; 58 + 59 + const handleLogin = async (e) => { 60 + e.preventDefault(); 61 + setError(''); 62 + try { 63 + const res = await axios.post('/api/login', { 64 + email: e.target.email.value, 65 + password: e.target.password.value 66 + }); 67 + localStorage.setItem('token', res.data.token); 68 + setToken(res.data.token); 69 + } catch (err) { 70 + setError('Invalid credentials'); 71 + } 72 + }; 73 + 74 + const handleRegister = async (e) => { 75 + e.preventDefault(); 76 + setError(''); 77 + try { 78 + await axios.post('/api/register', { 79 + email: e.target.email.value, 80 + password: e.target.password.value 81 + }); 82 + setView('login'); 83 + alert('Registration successful! Please login.'); 84 + } catch (err) { 85 + setError('User already exists'); 86 + } 87 + }; 88 + 89 + const handleLogout = () => { 90 + localStorage.removeItem('token'); 91 + setToken(null); 92 + setView('login'); 93 + }; 94 + 95 + const addMapping = async (e) => { 96 + e.preventDefault(); 97 + setLoading(true); 98 + try { 99 + const headers = { Authorization: `Bearer ${token}` }; 100 + await axios.post('/api/mappings', { 101 + owner: e.target.owner.value, 102 + twitterUsername: e.target.twitterUsername.value, 103 + bskyIdentifier: e.target.bskyIdentifier.value, 104 + bskyPassword: e.target.bskyPassword.value, 105 + bskyServiceUrl: e.target.bskyServiceUrl.value 106 + }, { headers }); 107 + fetchData(); 108 + e.target.reset(); 109 + } catch (err) { 110 + alert('Failed to add mapping'); 111 + } 112 + setLoading(false); 113 + }; 114 + 115 + const deleteMapping = async (id) => { 116 + if (!confirm('Are you sure?')) return; 117 + try { 118 + await axios.delete(`/api/mappings/${id}`, { 119 + headers: { Authorization: `Bearer ${token}` } 120 + }); 121 + fetchData(); 122 + } catch (err) { 123 + alert('Failed to delete'); 124 + } 125 + }; 126 + 127 + const updateTwitter = async (e) => { 128 + e.preventDefault(); 129 + try { 130 + await axios.post('/api/twitter-config', { 131 + authToken: e.target.authToken.value, 132 + ct0: e.target.ct0.value 133 + }, { headers: { Authorization: `Bearer ${token}` } }); 134 + alert('Global Twitter config updated!'); 135 + fetchData(); 136 + } catch (err) { 137 + alert('Failed to update twitter config'); 138 + } 139 + }; 140 + 141 + if (!token) { 142 + return ( 143 + <div className="container login-container"> 144 + <div className="card p-4"> 145 + <h2 className="text-center mb-4">{view === 'login' ? 'Login' : 'Register'}</h2> 146 + {error && <div className="alert alert-danger">{error}</div>} 147 + <form onSubmit={view === 'login' ? handleLogin : handleRegister}> 148 + <div className="mb-3"> 149 + <label className="form-label">Email</label> 150 + <input name="email" type="email" className="form-control" required /> 151 + </div> 152 + <div className="mb-3"> 153 + <label className="form-label">Password</label> 154 + <input name="password" type="password" className="form-control" required /> 155 + </div> 156 + <button type="submit" className="btn btn-primary w-100 mb-3"> 157 + {view === 'login' ? 'Login' : 'Register'} 158 + </button> 159 + <div className="text-center"> 160 + <a href="#" onClick={() => setView(view === 'login' ? 'register' : 'login')}> 161 + {view === 'login' ? 'Need an account? Register' : 'Have an account? Login'} 162 + </a> 163 + </div> 164 + </form> 165 + </div> 166 + </div> 167 + ); 168 + } 169 + 170 + return ( 171 + <div> 172 + <nav className="navbar navbar-expand-lg mb-4"> 173 + <div className="container"> 174 + <span className="navbar-brand d-flex align-items-center"> 175 + <span className="material-icons me-2 text-primary">swap_calls</span> 176 + <strong>Tweets-2-Bsky</strong> 177 + </span> 178 + <button className="btn btn-outline-danger btn-sm" onClick={handleLogout}>Logout</button> 179 + </div> 180 + </nav> 181 + 182 + <div className="container"> 183 + <div className="row"> 184 + {/* Left Column: Twitter Config */} 185 + <div className="col-md-4 mb-4"> 186 + <div className="card p-3 mb-4"> 187 + <h5 className="mb-3 d-flex align-items-center"> 188 + <span className="material-icons me-2">settings</span> Global Twitter Config 189 + </h5> 190 + <p className="text-muted small">This one set of cookies is used for all crossposting tasks. Recommend using a burner account.</p> 191 + <form onSubmit={updateTwitter}> 192 + <div className="mb-3"> 193 + <label className="form-label small">Auth Token</label> 194 + <input name="authToken" defaultValue={twitterConfig.authToken} className="form-control form-control-sm" required /> 195 + </div> 196 + <div className="mb-3"> 197 + <label className="form-label small">CT0</label> 198 + <input name="ct0" defaultValue={twitterConfig.ct0} className="form-control form-control-sm" required /> 199 + </div> 200 + <button className="btn btn-primary btn-sm w-100">Update Cookies</button> 201 + </form> 202 + </div> 203 + 204 + <div className="card p-3"> 205 + <h5 className="mb-3 d-flex align-items-center"> 206 + <span className="material-icons me-2">add_circle</span> Add New Mapping 207 + </h5> 208 + <form onSubmit={addMapping}> 209 + <div className="mb-2"> 210 + <input name="owner" placeholder="Owner Name (e.g. Dan)" className="form-control form-control-sm" required /> 211 + </div> 212 + <div className="mb-2"> 213 + <input name="twitterUsername" placeholder="Twitter Username" className="form-control form-control-sm" required /> 214 + </div> 215 + <hr/> 216 + <div className="mb-2"> 217 + <input name="bskyIdentifier" placeholder="Bluesky Handle/Email" className="form-control form-control-sm" required /> 218 + </div> 219 + <div className="mb-2"> 220 + <input name="bskyPassword" type="password" placeholder="Bluesky App Password" className="form-control form-control-sm" required /> 221 + </div> 222 + <div className="mb-3"> 223 + <input name="bskyServiceUrl" defaultValue="https://bsky.social" className="form-control form-control-sm" required /> 224 + </div> 225 + <button className="btn btn-success btn-sm w-100" disabled={loading}> 226 + {loading ? 'Adding...' : 'Add Account'} 227 + </button> 228 + </form> 229 + </div> 230 + </div> 231 + 232 + {/* Right Column: Active Mappings */} 233 + <div className="col-md-8"> 234 + <div className="card p-3"> 235 + <h5 className="mb-4 d-flex align-items-center"> 236 + <span className="material-icons me-2">list</span> Active Sync Tasks 237 + </h5> 238 + {mappings.length === 0 && <p className="text-center text-muted">No accounts added yet.</p>} 239 + <div className="table-responsive"> 240 + <table className="table align-middle"> 241 + <thead> 242 + <tr> 243 + <th>Owner</th> 244 + <th>Twitter</th> 245 + <th>Bluesky</th> 246 + <th>Status</th> 247 + <th></th> 248 + </tr> 249 + </thead> 250 + <tbody> 251 + {mappings.map(m => ( 252 + <tr key={m.id}> 253 + <td><span className="owner-badge">{m.owner || 'System'}</span></td> 254 + <td>@{m.twitterUsername}</td> 255 + <td className="small text-muted">{m.bskyIdentifier}</td> 256 + <td><span className="badge bg-success">Active</span></td> 257 + <td className="text-end"> 258 + <button onClick={() => deleteMapping(m.id)} className="btn btn-link text-danger p-0"> 259 + <span className="material-icons">delete</span> 260 + </button> 261 + </td> 262 + </tr> 263 + ))} 264 + </tbody> 265 + </table> 266 + </div> 267 + </div> 268 + </div> 269 + </div> 270 + </div> 271 + </div> 272 + ); 273 + } 274 + 275 + const root = ReactDOM.createRoot(document.getElementById('root')); 276 + root.render(<App />); 277 + </script> 278 + </body> 279 + </html>
+12 -2
src/config-manager.ts
··· 12 12 ct0: string; 13 13 } 14 14 15 + export interface WebUser { 16 + email: string; 17 + passwordHash: string; 18 + } 19 + 15 20 export interface AccountMapping { 16 21 id: string; 17 22 twitterUsername: string; ··· 19 24 bskyPassword: string; 20 25 bskyServiceUrl?: string; 21 26 enabled: boolean; 27 + owner?: string; 22 28 } 23 29 24 30 export interface AppConfig { 25 31 twitter: TwitterConfig; 26 32 mappings: AccountMapping[]; 33 + users: WebUser[]; 27 34 checkIntervalMinutes: number; 28 35 } 29 36 ··· 32 39 return { 33 40 twitter: { authToken: '', ct0: '' }, 34 41 mappings: [], 42 + users: [], 35 43 checkIntervalMinutes: 5, 36 44 }; 37 45 } 38 46 try { 39 - return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); 47 + const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); 48 + if (!config.users) config.users = []; 49 + return config; 40 50 } catch (err) { 41 51 console.error('Error reading config:', err); 42 52 return { 43 53 twitter: { authToken: '', ct0: '' }, 44 54 mappings: [], 55 + users: [], 45 56 checkIntervalMinutes: 5, 46 57 }; 47 58 } 48 59 } 49 - 50 60 export function saveConfig(config: AppConfig): void { 51 61 fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); 52 62 }
+55 -22
src/index.ts
··· 17 17 const __filename = fileURLToPath(import.meta.url); 18 18 const __dirname = path.dirname(__filename); 19 19 20 - // ============================================================================ 20 + // ============================================================================ 21 21 // Type Definitions 22 - // ============================================================================ 22 + // ============================================================================ 23 23 24 24 interface ProcessedTweetEntry { 25 25 uri?: string; ··· 111 111 aspectRatio?: AspectRatio; 112 112 } 113 113 114 - // ============================================================================ 114 + // ============================================================================ 115 115 // State Management 116 - // ============================================================================ 116 + // ============================================================================ 117 117 118 118 const PROCESSED_DIR = path.join(__dirname, '..', 'processed'); 119 119 if (!fs.existsSync(PROCESSED_DIR)) { ··· 145 145 } 146 146 } 147 147 148 - // ============================================================================ 148 + // ============================================================================ 149 149 // Custom Twitter Client 150 - // ============================================================================ 150 + // ============================================================================ 151 151 152 152 interface TwitterLegacyResult { 153 153 legacy?: { ··· 178 178 179 179 let twitter: CustomTwitterClient; 180 180 181 - // ============================================================================ 181 + // ============================================================================ 182 182 // Helper Functions 183 - // ============================================================================ 183 + // ============================================================================ 184 184 185 185 function detectLanguage(text: string): string[] { 186 186 if (!text || text.trim().length === 0) return ['en']; ··· 283 283 } 284 284 } 285 285 286 - // ============================================================================ 286 + // ============================================================================ 287 287 // Main Processing Logic 288 - // ============================================================================ 288 + // ============================================================================ 289 289 290 290 async function processTweets( 291 291 agent: BskyAgent, ··· 556 556 } 557 557 } 558 558 559 + import { startServer } from './server.js'; 560 + 561 + // ... (previous imports) 562 + 559 563 async function main(): Promise<void> { 560 564 const program = new Command(); 561 565 program 562 566 .name('tweets-2-bsky') 563 567 .description('Crosspost tweets to Bluesky') 564 568 .option('--dry-run', 'Fetch tweets but do not post to Bluesky', false) 569 + .option('--no-web', 'Disable the web interface') 565 570 .option('--import-history', 'Run in history import mode') 566 571 .option('--username <username>', 'Twitter username for history import') 567 572 .option('--limit <number>', 'Limit the number of tweets to import', (val) => Number.parseInt(val, 10)) ··· 570 575 const options = program.opts(); 571 576 572 577 const config = getConfig(); 573 - if (!config.twitter.authToken || !config.twitter.ct0) { 574 - console.error('Twitter credentials not set. Use "npm run cli setup-twitter".'); 575 - process.exit(1); 578 + 579 + if (!options.web) { 580 + console.log('🌐 Web interface is disabled.'); 581 + } else { 582 + startServer(); 583 + if (config.users.length === 0) { 584 + console.log('ℹ️ No users found. Please register on the web interface to get started.'); 585 + } 576 586 } 577 587 578 - twitter = new CustomTwitterClient({ 579 - cookies: { 580 - authToken: config.twitter.authToken, 581 - ct0: config.twitter.ct0, 582 - }, 583 - }); 588 + // Allow starting even if twitter credentials are not set (can be set via web UI now) 589 + const twitterConfigured = config.twitter.authToken && config.twitter.ct0; 590 + 591 + if (twitterConfigured) { 592 + twitter = new CustomTwitterClient({ 593 + cookies: { 594 + authToken: config.twitter.authToken, 595 + ct0: config.twitter.ct0, 596 + }, 597 + }); 598 + } else { 599 + console.warn('⚠️ Twitter credentials not set. Use the web UI or CLI to configure them.'); 600 + } 584 601 585 602 if (options.importHistory) { 586 603 if (!options.username) { ··· 591 608 process.exit(0); 592 609 } 593 610 594 - await checkAndPost(options.dryRun); 611 + if (twitter) { 612 + await checkAndPost(options.dryRun); 613 + } 595 614 596 615 if (options.dryRun) { 597 616 console.log('Dry run complete. Exiting.'); ··· 600 619 601 620 console.log(`Scheduling check every ${config.checkIntervalMinutes} minutes.`); 602 621 cron.schedule(`*/${config.checkIntervalMinutes} * * * *`, () => { 603 - checkAndPost(options.dryRun); 622 + if (twitter) { 623 + checkAndPost(options.dryRun); 624 + } else { 625 + // Try to re-initialize if config was updated via web 626 + const currentConfig = getConfig(); 627 + if (currentConfig.twitter.authToken && currentConfig.twitter.ct0) { 628 + twitter = new CustomTwitterClient({ 629 + cookies: { 630 + authToken: currentConfig.twitter.authToken, 631 + ct0: currentConfig.twitter.ct0, 632 + }, 633 + }); 634 + checkAndPost(options.dryRun); 635 + } 636 + } 604 637 }); 605 638 } 606 639 607 - main(); 640 + main();
+127
src/server.ts
··· 1 + import path from 'node:path'; 2 + import { fileURLToPath } from 'node:url'; 3 + import bcrypt from 'bcryptjs'; 4 + import cors from 'cors'; 5 + import express from 'express'; 6 + import jwt from 'jsonwebtoken'; 7 + import { getConfig, saveConfig } from './config-manager.js'; 8 + 9 + const __filename = fileURLToPath(import.meta.url); 10 + const __dirname = path.dirname(__filename); 11 + 12 + const app = express(); 13 + const PORT = Number(process.env.PORT) || 3000; 14 + const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret'; 15 + 16 + app.use(cors()); 17 + app.use(express.json()); 18 + 19 + // Serve static files from the React app (we will build this later) 20 + app.use(express.static(path.join(__dirname, '../public'))); 21 + 22 + // Middleware to protect routes 23 + const authenticateToken = (req: any, res: any, next: any) => { 24 + const authHeader = req.headers.authorization; 25 + const token = authHeader?.split(' ')[1]; 26 + 27 + if (!token) return res.sendStatus(401); 28 + 29 + jwt.verify(token, JWT_SECRET, (err: any, user: any) => { 30 + if (err) return res.sendStatus(403); 31 + req.user = user; 32 + next(); 33 + }); 34 + }; 35 + 36 + // --- Auth Routes --- 37 + 38 + app.post('/api/register', async (req, res) => { 39 + const { email, password } = req.body; 40 + const config = getConfig(); 41 + 42 + if (config.users.find((u) => u.email === email)) { 43 + res.status(400).json({ error: 'User already exists' }); 44 + return; 45 + } 46 + 47 + const passwordHash = await bcrypt.hash(password, 10); 48 + config.users.push({ email, passwordHash }); 49 + saveConfig(config); 50 + 51 + res.json({ success: true }); 52 + }); 53 + 54 + app.post('/api/login', async (req, res) => { 55 + const { email, password } = req.body; 56 + const config = getConfig(); 57 + const user = config.users.find((u) => u.email === email); 58 + 59 + if (!user || !(await bcrypt.compare(password, user.passwordHash))) { 60 + res.status(401).json({ error: 'Invalid credentials' }); 61 + return; 62 + } 63 + 64 + const token = jwt.sign({ email: user.email }, JWT_SECRET, { expiresIn: '24h' }); 65 + res.json({ token }); 66 + }); 67 + 68 + // --- Mapping Routes --- 69 + 70 + app.get('/api/mappings', authenticateToken, (_req, res) => { 71 + const config = getConfig(); 72 + res.json(config.mappings); 73 + }); 74 + 75 + app.post('/api/mappings', authenticateToken, (req, res) => { 76 + const { twitterUsername, bskyIdentifier, bskyPassword, bskyServiceUrl, owner } = req.body; 77 + const config = getConfig(); 78 + 79 + const newMapping = { 80 + id: Math.random().toString(36).substring(7), 81 + twitterUsername, 82 + bskyIdentifier, 83 + bskyPassword, 84 + bskyServiceUrl: bskyServiceUrl || 'https://bsky.social', 85 + enabled: true, 86 + owner, 87 + }; 88 + 89 + config.mappings.push(newMapping); 90 + saveConfig(config); 91 + res.json(newMapping); 92 + }); 93 + 94 + app.delete('/api/mappings/:id', authenticateToken, (req, res) => { 95 + const { id } = req.params; 96 + const config = getConfig(); 97 + config.mappings = config.mappings.filter((m) => m.id !== id); 98 + saveConfig(config); 99 + res.json({ success: true }); 100 + }); 101 + 102 + // --- Twitter Config Routes --- 103 + 104 + app.get('/api/twitter-config', authenticateToken, (_req, res) => { 105 + const config = getConfig(); 106 + res.json(config.twitter); 107 + }); 108 + 109 + app.post('/api/twitter-config', authenticateToken, (req, res) => { 110 + const { authToken, ct0 } = req.body; 111 + const config = getConfig(); 112 + config.twitter = { authToken, ct0 }; 113 + saveConfig(config); 114 + res.json({ success: true }); 115 + }); 116 + 117 + // Serve the frontend for any other route 118 + app.get('*', (_req, res) => { 119 + res.sendFile(path.join(__dirname, '../public/index.html')); 120 + }); 121 + 122 + export function startServer() { 123 + app.listen(PORT, '0.0.0.0' as any, () => { 124 + console.log(`🚀 Web interface running at http://localhost:${PORT}`); 125 + console.log('📡 Accessible on your local network/Tailscale via your IP.'); 126 + }); 127 + }