Spaces:
Runtime error
Runtime error
incognitolm commited on
Commit ·
bff1056
1
Parent(s): c97ad08
Migration to PostgreSQL
Browse files- .gitignore +1 -0
- package-lock.json +2076 -0
- package.json +4 -2
- scripts/migrate-to-postgres.js +573 -0
- server/auth.js +85 -0
- server/chatTrashStore.js +97 -3
- server/cryptoUtils.js +7 -0
- server/dataPaths.js +13 -0
- server/guestRequestLimiter.js +94 -3
- server/handleFeedback.js +38 -1
- server/index.js +12 -64
- server/mediaStore.js +158 -16
- server/memoryStore.js +73 -4
- server/postgres.js +132 -0
- server/postgresSchema.js +148 -0
- server/sessionStore.js +577 -91
- server/systemPromptStore.js +53 -1
- server/versionStore.js +85 -0
- server/webSearchUsageStore.js +95 -5
- server/wsHandler.js +46 -38
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
package-lock.json
ADDED
|
@@ -0,0 +1,2076 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "inferenceport-web",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "inferenceport-web",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@gradio/client": "^0.20.1",
|
| 12 |
+
"@supabase/supabase-js": "^2.39.7",
|
| 13 |
+
"express": "^4.18.2",
|
| 14 |
+
"express-rate-limit": "8.3.2",
|
| 15 |
+
"html-to-text": "^9.0.5",
|
| 16 |
+
"node-fetch": "^3.3.2",
|
| 17 |
+
"openai": "^6.29.0",
|
| 18 |
+
"pg": "^8.16.3",
|
| 19 |
+
"tiktoken": "^1.0.0",
|
| 20 |
+
"ws": "^8.16.0"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"node_modules/@gradio/client": {
|
| 24 |
+
"version": "0.20.1",
|
| 25 |
+
"resolved": "https://registry.npmjs.org/@gradio/client/-/client-0.20.1.tgz",
|
| 26 |
+
"integrity": "sha512-c+qB63M36vEkDD2o8K+Pkb/JhujQF5IbD1ecc7KFI+czKs5ojGlPsyPdlhxWGNg/rkl7Ei1MQm+pTIF+j0n0Lg==",
|
| 27 |
+
"license": "ISC",
|
| 28 |
+
"dependencies": {
|
| 29 |
+
"@types/eventsource": "^1.1.15",
|
| 30 |
+
"bufferutil": "^4.0.7",
|
| 31 |
+
"eventsource": "^2.0.2",
|
| 32 |
+
"msw": "^2.2.1",
|
| 33 |
+
"semiver": "^1.1.0",
|
| 34 |
+
"typescript": "^5.0.0",
|
| 35 |
+
"ws": "^8.13.0"
|
| 36 |
+
},
|
| 37 |
+
"engines": {
|
| 38 |
+
"node": ">=18.0.0"
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"node_modules/@inquirer/ansi": {
|
| 42 |
+
"version": "2.0.5",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz",
|
| 44 |
+
"integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==",
|
| 45 |
+
"license": "MIT",
|
| 46 |
+
"engines": {
|
| 47 |
+
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"node_modules/@inquirer/confirm": {
|
| 51 |
+
"version": "6.0.12",
|
| 52 |
+
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz",
|
| 53 |
+
"integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==",
|
| 54 |
+
"license": "MIT",
|
| 55 |
+
"dependencies": {
|
| 56 |
+
"@inquirer/core": "^11.1.9",
|
| 57 |
+
"@inquirer/type": "^4.0.5"
|
| 58 |
+
},
|
| 59 |
+
"engines": {
|
| 60 |
+
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
|
| 61 |
+
},
|
| 62 |
+
"peerDependencies": {
|
| 63 |
+
"@types/node": ">=18"
|
| 64 |
+
},
|
| 65 |
+
"peerDependenciesMeta": {
|
| 66 |
+
"@types/node": {
|
| 67 |
+
"optional": true
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"node_modules/@inquirer/core": {
|
| 72 |
+
"version": "11.1.9",
|
| 73 |
+
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz",
|
| 74 |
+
"integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==",
|
| 75 |
+
"license": "MIT",
|
| 76 |
+
"dependencies": {
|
| 77 |
+
"@inquirer/ansi": "^2.0.5",
|
| 78 |
+
"@inquirer/figures": "^2.0.5",
|
| 79 |
+
"@inquirer/type": "^4.0.5",
|
| 80 |
+
"cli-width": "^4.1.0",
|
| 81 |
+
"fast-wrap-ansi": "^0.2.0",
|
| 82 |
+
"mute-stream": "^3.0.0",
|
| 83 |
+
"signal-exit": "^4.1.0"
|
| 84 |
+
},
|
| 85 |
+
"engines": {
|
| 86 |
+
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
|
| 87 |
+
},
|
| 88 |
+
"peerDependencies": {
|
| 89 |
+
"@types/node": ">=18"
|
| 90 |
+
},
|
| 91 |
+
"peerDependenciesMeta": {
|
| 92 |
+
"@types/node": {
|
| 93 |
+
"optional": true
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"node_modules/@inquirer/figures": {
|
| 98 |
+
"version": "2.0.5",
|
| 99 |
+
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz",
|
| 100 |
+
"integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==",
|
| 101 |
+
"license": "MIT",
|
| 102 |
+
"engines": {
|
| 103 |
+
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
"node_modules/@inquirer/type": {
|
| 107 |
+
"version": "4.0.5",
|
| 108 |
+
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz",
|
| 109 |
+
"integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==",
|
| 110 |
+
"license": "MIT",
|
| 111 |
+
"engines": {
|
| 112 |
+
"node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
|
| 113 |
+
},
|
| 114 |
+
"peerDependencies": {
|
| 115 |
+
"@types/node": ">=18"
|
| 116 |
+
},
|
| 117 |
+
"peerDependenciesMeta": {
|
| 118 |
+
"@types/node": {
|
| 119 |
+
"optional": true
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"node_modules/@mswjs/interceptors": {
|
| 124 |
+
"version": "0.41.4",
|
| 125 |
+
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.4.tgz",
|
| 126 |
+
"integrity": "sha512-3B9EinUkrdOUGYzHRzRWSXunQ4YFGboJnyLNRwEJWEde+j8fNhPUHvrN1E3g1DU/iS/s8JQrMNVe+S7AHHVs0w==",
|
| 127 |
+
"license": "MIT",
|
| 128 |
+
"dependencies": {
|
| 129 |
+
"@open-draft/deferred-promise": "^2.2.0",
|
| 130 |
+
"@open-draft/logger": "^0.3.0",
|
| 131 |
+
"@open-draft/until": "^2.0.0",
|
| 132 |
+
"is-node-process": "^1.2.0",
|
| 133 |
+
"outvariant": "^1.4.3",
|
| 134 |
+
"strict-event-emitter": "^0.5.1"
|
| 135 |
+
},
|
| 136 |
+
"engines": {
|
| 137 |
+
"node": ">=18"
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
"node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": {
|
| 141 |
+
"version": "2.2.0",
|
| 142 |
+
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
|
| 143 |
+
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
|
| 144 |
+
"license": "MIT"
|
| 145 |
+
},
|
| 146 |
+
"node_modules/@open-draft/deferred-promise": {
|
| 147 |
+
"version": "3.0.0",
|
| 148 |
+
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz",
|
| 149 |
+
"integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==",
|
| 150 |
+
"license": "MIT"
|
| 151 |
+
},
|
| 152 |
+
"node_modules/@open-draft/logger": {
|
| 153 |
+
"version": "0.3.0",
|
| 154 |
+
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
|
| 155 |
+
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
|
| 156 |
+
"license": "MIT",
|
| 157 |
+
"dependencies": {
|
| 158 |
+
"is-node-process": "^1.2.0",
|
| 159 |
+
"outvariant": "^1.4.0"
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
"node_modules/@open-draft/until": {
|
| 163 |
+
"version": "2.1.0",
|
| 164 |
+
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
|
| 165 |
+
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
| 166 |
+
"license": "MIT"
|
| 167 |
+
},
|
| 168 |
+
"node_modules/@selderee/plugin-htmlparser2": {
|
| 169 |
+
"version": "0.11.0",
|
| 170 |
+
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
| 171 |
+
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
| 172 |
+
"license": "MIT",
|
| 173 |
+
"dependencies": {
|
| 174 |
+
"domhandler": "^5.0.3",
|
| 175 |
+
"selderee": "^0.11.0"
|
| 176 |
+
},
|
| 177 |
+
"funding": {
|
| 178 |
+
"url": "https://ko-fi.com/killymxi"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"node_modules/@supabase/auth-js": {
|
| 182 |
+
"version": "2.104.0",
|
| 183 |
+
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.104.0.tgz",
|
| 184 |
+
"integrity": "sha512-Vs0ndL+s5an7rOmXtS/nbYnGXL8m+KXlCSrPIcw9bR96ma6qyLYILnE6syuM+rpDnf+Tg4PVNxNB2+oDwoy6mA==",
|
| 185 |
+
"license": "MIT",
|
| 186 |
+
"dependencies": {
|
| 187 |
+
"tslib": "2.8.1"
|
| 188 |
+
},
|
| 189 |
+
"engines": {
|
| 190 |
+
"node": ">=20.0.0"
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
"node_modules/@supabase/functions-js": {
|
| 194 |
+
"version": "2.104.0",
|
| 195 |
+
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.104.0.tgz",
|
| 196 |
+
"integrity": "sha512-O8EyEz/RT1kfWhyJNpVc/VbLeBsohHGBVif/CI83zoMB+Iul/t/NIekH1/7RsH6kuO+b2D4wJhfiaW8Qr47sRg==",
|
| 197 |
+
"license": "MIT",
|
| 198 |
+
"dependencies": {
|
| 199 |
+
"tslib": "2.8.1"
|
| 200 |
+
},
|
| 201 |
+
"engines": {
|
| 202 |
+
"node": ">=20.0.0"
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"node_modules/@supabase/phoenix": {
|
| 206 |
+
"version": "0.4.0",
|
| 207 |
+
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz",
|
| 208 |
+
"integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==",
|
| 209 |
+
"license": "MIT"
|
| 210 |
+
},
|
| 211 |
+
"node_modules/@supabase/postgrest-js": {
|
| 212 |
+
"version": "2.104.0",
|
| 213 |
+
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.104.0.tgz",
|
| 214 |
+
"integrity": "sha512-ynylEq6wduQEycj6pL3P+/yIfDQ+CTnBC5I6p+PzcAO2ybj9coAITVtMfboi+g/dacgMslN5MH73rXsRMB29+Q==",
|
| 215 |
+
"license": "MIT",
|
| 216 |
+
"dependencies": {
|
| 217 |
+
"tslib": "2.8.1"
|
| 218 |
+
},
|
| 219 |
+
"engines": {
|
| 220 |
+
"node": ">=20.0.0"
|
| 221 |
+
}
|
| 222 |
+
},
|
| 223 |
+
"node_modules/@supabase/realtime-js": {
|
| 224 |
+
"version": "2.104.0",
|
| 225 |
+
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.104.0.tgz",
|
| 226 |
+
"integrity": "sha512-9fUVDoTVAhn7a79+AmEx+asUlRtf2yBrji7TQckcKn/WK4hvAA9Lia9er+lnhuz3WNiF1x6kkA4x7bRCJrU+KA==",
|
| 227 |
+
"license": "MIT",
|
| 228 |
+
"dependencies": {
|
| 229 |
+
"@supabase/phoenix": "^0.4.0",
|
| 230 |
+
"@types/ws": "^8.18.1",
|
| 231 |
+
"tslib": "2.8.1",
|
| 232 |
+
"ws": "^8.18.2"
|
| 233 |
+
},
|
| 234 |
+
"engines": {
|
| 235 |
+
"node": ">=20.0.0"
|
| 236 |
+
}
|
| 237 |
+
},
|
| 238 |
+
"node_modules/@supabase/storage-js": {
|
| 239 |
+
"version": "2.104.0",
|
| 240 |
+
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.104.0.tgz",
|
| 241 |
+
"integrity": "sha512-s2NHtuAWb9nldJ/fS62WnJE6edvCWn31rrO+FJKlAohs99qdVgtLegUReTU2H9WnZiQlVqaBtu386wt6/6lrRw==",
|
| 242 |
+
"license": "MIT",
|
| 243 |
+
"dependencies": {
|
| 244 |
+
"iceberg-js": "^0.8.1",
|
| 245 |
+
"tslib": "2.8.1"
|
| 246 |
+
},
|
| 247 |
+
"engines": {
|
| 248 |
+
"node": ">=20.0.0"
|
| 249 |
+
}
|
| 250 |
+
},
|
| 251 |
+
"node_modules/@supabase/supabase-js": {
|
| 252 |
+
"version": "2.104.0",
|
| 253 |
+
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.104.0.tgz",
|
| 254 |
+
"integrity": "sha512-hILwhIjCB53G31jlHUe73NDEmrXudcjcYlVRuvNfEhzf0gyFQaFf7j6rd1UGmYZkFMOg//DFE8Iy9ZbNEgosVw==",
|
| 255 |
+
"license": "MIT",
|
| 256 |
+
"dependencies": {
|
| 257 |
+
"@supabase/auth-js": "2.104.0",
|
| 258 |
+
"@supabase/functions-js": "2.104.0",
|
| 259 |
+
"@supabase/postgrest-js": "2.104.0",
|
| 260 |
+
"@supabase/realtime-js": "2.104.0",
|
| 261 |
+
"@supabase/storage-js": "2.104.0"
|
| 262 |
+
},
|
| 263 |
+
"engines": {
|
| 264 |
+
"node": ">=20.0.0"
|
| 265 |
+
}
|
| 266 |
+
},
|
| 267 |
+
"node_modules/@types/eventsource": {
|
| 268 |
+
"version": "1.1.15",
|
| 269 |
+
"resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz",
|
| 270 |
+
"integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==",
|
| 271 |
+
"license": "MIT"
|
| 272 |
+
},
|
| 273 |
+
"node_modules/@types/node": {
|
| 274 |
+
"version": "25.6.0",
|
| 275 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
| 276 |
+
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
| 277 |
+
"license": "MIT",
|
| 278 |
+
"dependencies": {
|
| 279 |
+
"undici-types": "~7.19.0"
|
| 280 |
+
}
|
| 281 |
+
},
|
| 282 |
+
"node_modules/@types/set-cookie-parser": {
|
| 283 |
+
"version": "2.4.10",
|
| 284 |
+
"resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz",
|
| 285 |
+
"integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==",
|
| 286 |
+
"license": "MIT",
|
| 287 |
+
"dependencies": {
|
| 288 |
+
"@types/node": "*"
|
| 289 |
+
}
|
| 290 |
+
},
|
| 291 |
+
"node_modules/@types/statuses": {
|
| 292 |
+
"version": "2.0.6",
|
| 293 |
+
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
|
| 294 |
+
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
|
| 295 |
+
"license": "MIT"
|
| 296 |
+
},
|
| 297 |
+
"node_modules/@types/ws": {
|
| 298 |
+
"version": "8.18.1",
|
| 299 |
+
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
| 300 |
+
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
| 301 |
+
"license": "MIT",
|
| 302 |
+
"dependencies": {
|
| 303 |
+
"@types/node": "*"
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
"node_modules/accepts": {
|
| 307 |
+
"version": "1.3.8",
|
| 308 |
+
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
| 309 |
+
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
| 310 |
+
"license": "MIT",
|
| 311 |
+
"dependencies": {
|
| 312 |
+
"mime-types": "~2.1.34",
|
| 313 |
+
"negotiator": "0.6.3"
|
| 314 |
+
},
|
| 315 |
+
"engines": {
|
| 316 |
+
"node": ">= 0.6"
|
| 317 |
+
}
|
| 318 |
+
},
|
| 319 |
+
"node_modules/ansi-regex": {
|
| 320 |
+
"version": "5.0.1",
|
| 321 |
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
| 322 |
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
| 323 |
+
"license": "MIT",
|
| 324 |
+
"engines": {
|
| 325 |
+
"node": ">=8"
|
| 326 |
+
}
|
| 327 |
+
},
|
| 328 |
+
"node_modules/ansi-styles": {
|
| 329 |
+
"version": "4.3.0",
|
| 330 |
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
| 331 |
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
| 332 |
+
"license": "MIT",
|
| 333 |
+
"dependencies": {
|
| 334 |
+
"color-convert": "^2.0.1"
|
| 335 |
+
},
|
| 336 |
+
"engines": {
|
| 337 |
+
"node": ">=8"
|
| 338 |
+
},
|
| 339 |
+
"funding": {
|
| 340 |
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
"node_modules/array-flatten": {
|
| 344 |
+
"version": "1.1.1",
|
| 345 |
+
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
| 346 |
+
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
| 347 |
+
"license": "MIT"
|
| 348 |
+
},
|
| 349 |
+
"node_modules/body-parser": {
|
| 350 |
+
"version": "1.20.4",
|
| 351 |
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
| 352 |
+
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
| 353 |
+
"license": "MIT",
|
| 354 |
+
"dependencies": {
|
| 355 |
+
"bytes": "~3.1.2",
|
| 356 |
+
"content-type": "~1.0.5",
|
| 357 |
+
"debug": "2.6.9",
|
| 358 |
+
"depd": "2.0.0",
|
| 359 |
+
"destroy": "~1.2.0",
|
| 360 |
+
"http-errors": "~2.0.1",
|
| 361 |
+
"iconv-lite": "~0.4.24",
|
| 362 |
+
"on-finished": "~2.4.1",
|
| 363 |
+
"qs": "~6.14.0",
|
| 364 |
+
"raw-body": "~2.5.3",
|
| 365 |
+
"type-is": "~1.6.18",
|
| 366 |
+
"unpipe": "~1.0.0"
|
| 367 |
+
},
|
| 368 |
+
"engines": {
|
| 369 |
+
"node": ">= 0.8",
|
| 370 |
+
"npm": "1.2.8000 || >= 1.4.16"
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
"node_modules/bufferutil": {
|
| 374 |
+
"version": "4.1.0",
|
| 375 |
+
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
|
| 376 |
+
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
|
| 377 |
+
"hasInstallScript": true,
|
| 378 |
+
"license": "MIT",
|
| 379 |
+
"dependencies": {
|
| 380 |
+
"node-gyp-build": "^4.3.0"
|
| 381 |
+
},
|
| 382 |
+
"engines": {
|
| 383 |
+
"node": ">=6.14.2"
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
"node_modules/bytes": {
|
| 387 |
+
"version": "3.1.2",
|
| 388 |
+
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
| 389 |
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
| 390 |
+
"license": "MIT",
|
| 391 |
+
"engines": {
|
| 392 |
+
"node": ">= 0.8"
|
| 393 |
+
}
|
| 394 |
+
},
|
| 395 |
+
"node_modules/call-bind-apply-helpers": {
|
| 396 |
+
"version": "1.0.2",
|
| 397 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 398 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 399 |
+
"license": "MIT",
|
| 400 |
+
"dependencies": {
|
| 401 |
+
"es-errors": "^1.3.0",
|
| 402 |
+
"function-bind": "^1.1.2"
|
| 403 |
+
},
|
| 404 |
+
"engines": {
|
| 405 |
+
"node": ">= 0.4"
|
| 406 |
+
}
|
| 407 |
+
},
|
| 408 |
+
"node_modules/call-bound": {
|
| 409 |
+
"version": "1.0.4",
|
| 410 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
| 411 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
| 412 |
+
"license": "MIT",
|
| 413 |
+
"dependencies": {
|
| 414 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 415 |
+
"get-intrinsic": "^1.3.0"
|
| 416 |
+
},
|
| 417 |
+
"engines": {
|
| 418 |
+
"node": ">= 0.4"
|
| 419 |
+
},
|
| 420 |
+
"funding": {
|
| 421 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 422 |
+
}
|
| 423 |
+
},
|
| 424 |
+
"node_modules/cli-width": {
|
| 425 |
+
"version": "4.1.0",
|
| 426 |
+
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
| 427 |
+
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
| 428 |
+
"license": "ISC",
|
| 429 |
+
"engines": {
|
| 430 |
+
"node": ">= 12"
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"node_modules/cliui": {
|
| 434 |
+
"version": "8.0.1",
|
| 435 |
+
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
| 436 |
+
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
| 437 |
+
"license": "ISC",
|
| 438 |
+
"dependencies": {
|
| 439 |
+
"string-width": "^4.2.0",
|
| 440 |
+
"strip-ansi": "^6.0.1",
|
| 441 |
+
"wrap-ansi": "^7.0.0"
|
| 442 |
+
},
|
| 443 |
+
"engines": {
|
| 444 |
+
"node": ">=12"
|
| 445 |
+
}
|
| 446 |
+
},
|
| 447 |
+
"node_modules/color-convert": {
|
| 448 |
+
"version": "2.0.1",
|
| 449 |
+
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
| 450 |
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
| 451 |
+
"license": "MIT",
|
| 452 |
+
"dependencies": {
|
| 453 |
+
"color-name": "~1.1.4"
|
| 454 |
+
},
|
| 455 |
+
"engines": {
|
| 456 |
+
"node": ">=7.0.0"
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"node_modules/color-name": {
|
| 460 |
+
"version": "1.1.4",
|
| 461 |
+
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
| 462 |
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
| 463 |
+
"license": "MIT"
|
| 464 |
+
},
|
| 465 |
+
"node_modules/content-disposition": {
|
| 466 |
+
"version": "0.5.4",
|
| 467 |
+
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
| 468 |
+
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
| 469 |
+
"license": "MIT",
|
| 470 |
+
"dependencies": {
|
| 471 |
+
"safe-buffer": "5.2.1"
|
| 472 |
+
},
|
| 473 |
+
"engines": {
|
| 474 |
+
"node": ">= 0.6"
|
| 475 |
+
}
|
| 476 |
+
},
|
| 477 |
+
"node_modules/content-type": {
|
| 478 |
+
"version": "1.0.5",
|
| 479 |
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
| 480 |
+
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
| 481 |
+
"license": "MIT",
|
| 482 |
+
"engines": {
|
| 483 |
+
"node": ">= 0.6"
|
| 484 |
+
}
|
| 485 |
+
},
|
| 486 |
+
"node_modules/cookie": {
|
| 487 |
+
"version": "0.7.2",
|
| 488 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
| 489 |
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
| 490 |
+
"license": "MIT",
|
| 491 |
+
"engines": {
|
| 492 |
+
"node": ">= 0.6"
|
| 493 |
+
}
|
| 494 |
+
},
|
| 495 |
+
"node_modules/cookie-signature": {
|
| 496 |
+
"version": "1.0.7",
|
| 497 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
| 498 |
+
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
| 499 |
+
"license": "MIT"
|
| 500 |
+
},
|
| 501 |
+
"node_modules/data-uri-to-buffer": {
|
| 502 |
+
"version": "4.0.1",
|
| 503 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
| 504 |
+
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
| 505 |
+
"license": "MIT",
|
| 506 |
+
"engines": {
|
| 507 |
+
"node": ">= 12"
|
| 508 |
+
}
|
| 509 |
+
},
|
| 510 |
+
"node_modules/debug": {
|
| 511 |
+
"version": "2.6.9",
|
| 512 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
| 513 |
+
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
| 514 |
+
"license": "MIT",
|
| 515 |
+
"dependencies": {
|
| 516 |
+
"ms": "2.0.0"
|
| 517 |
+
}
|
| 518 |
+
},
|
| 519 |
+
"node_modules/deepmerge": {
|
| 520 |
+
"version": "4.3.1",
|
| 521 |
+
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
| 522 |
+
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
| 523 |
+
"license": "MIT",
|
| 524 |
+
"engines": {
|
| 525 |
+
"node": ">=0.10.0"
|
| 526 |
+
}
|
| 527 |
+
},
|
| 528 |
+
"node_modules/depd": {
|
| 529 |
+
"version": "2.0.0",
|
| 530 |
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
| 531 |
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
| 532 |
+
"license": "MIT",
|
| 533 |
+
"engines": {
|
| 534 |
+
"node": ">= 0.8"
|
| 535 |
+
}
|
| 536 |
+
},
|
| 537 |
+
"node_modules/destroy": {
|
| 538 |
+
"version": "1.2.0",
|
| 539 |
+
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
| 540 |
+
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
| 541 |
+
"license": "MIT",
|
| 542 |
+
"engines": {
|
| 543 |
+
"node": ">= 0.8",
|
| 544 |
+
"npm": "1.2.8000 || >= 1.4.16"
|
| 545 |
+
}
|
| 546 |
+
},
|
| 547 |
+
"node_modules/dom-serializer": {
|
| 548 |
+
"version": "2.0.0",
|
| 549 |
+
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
| 550 |
+
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
| 551 |
+
"license": "MIT",
|
| 552 |
+
"dependencies": {
|
| 553 |
+
"domelementtype": "^2.3.0",
|
| 554 |
+
"domhandler": "^5.0.2",
|
| 555 |
+
"entities": "^4.2.0"
|
| 556 |
+
},
|
| 557 |
+
"funding": {
|
| 558 |
+
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
| 559 |
+
}
|
| 560 |
+
},
|
| 561 |
+
"node_modules/domelementtype": {
|
| 562 |
+
"version": "2.3.0",
|
| 563 |
+
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
| 564 |
+
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
| 565 |
+
"funding": [
|
| 566 |
+
{
|
| 567 |
+
"type": "github",
|
| 568 |
+
"url": "https://github.com/sponsors/fb55"
|
| 569 |
+
}
|
| 570 |
+
],
|
| 571 |
+
"license": "BSD-2-Clause"
|
| 572 |
+
},
|
| 573 |
+
"node_modules/domhandler": {
|
| 574 |
+
"version": "5.0.3",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
| 576 |
+
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
| 577 |
+
"license": "BSD-2-Clause",
|
| 578 |
+
"dependencies": {
|
| 579 |
+
"domelementtype": "^2.3.0"
|
| 580 |
+
},
|
| 581 |
+
"engines": {
|
| 582 |
+
"node": ">= 4"
|
| 583 |
+
},
|
| 584 |
+
"funding": {
|
| 585 |
+
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
| 586 |
+
}
|
| 587 |
+
},
|
| 588 |
+
"node_modules/domutils": {
|
| 589 |
+
"version": "3.2.2",
|
| 590 |
+
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
| 591 |
+
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
| 592 |
+
"license": "BSD-2-Clause",
|
| 593 |
+
"dependencies": {
|
| 594 |
+
"dom-serializer": "^2.0.0",
|
| 595 |
+
"domelementtype": "^2.3.0",
|
| 596 |
+
"domhandler": "^5.0.3"
|
| 597 |
+
},
|
| 598 |
+
"funding": {
|
| 599 |
+
"url": "https://github.com/fb55/domutils?sponsor=1"
|
| 600 |
+
}
|
| 601 |
+
},
|
| 602 |
+
"node_modules/dunder-proto": {
|
| 603 |
+
"version": "1.0.1",
|
| 604 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 605 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 606 |
+
"license": "MIT",
|
| 607 |
+
"dependencies": {
|
| 608 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 609 |
+
"es-errors": "^1.3.0",
|
| 610 |
+
"gopd": "^1.2.0"
|
| 611 |
+
},
|
| 612 |
+
"engines": {
|
| 613 |
+
"node": ">= 0.4"
|
| 614 |
+
}
|
| 615 |
+
},
|
| 616 |
+
"node_modules/ee-first": {
|
| 617 |
+
"version": "1.1.1",
|
| 618 |
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
| 619 |
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
| 620 |
+
"license": "MIT"
|
| 621 |
+
},
|
| 622 |
+
"node_modules/emoji-regex": {
|
| 623 |
+
"version": "8.0.0",
|
| 624 |
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
| 625 |
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
| 626 |
+
"license": "MIT"
|
| 627 |
+
},
|
| 628 |
+
"node_modules/encodeurl": {
|
| 629 |
+
"version": "2.0.0",
|
| 630 |
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
| 631 |
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
| 632 |
+
"license": "MIT",
|
| 633 |
+
"engines": {
|
| 634 |
+
"node": ">= 0.8"
|
| 635 |
+
}
|
| 636 |
+
},
|
| 637 |
+
"node_modules/entities": {
|
| 638 |
+
"version": "4.5.0",
|
| 639 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
| 640 |
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
| 641 |
+
"license": "BSD-2-Clause",
|
| 642 |
+
"engines": {
|
| 643 |
+
"node": ">=0.12"
|
| 644 |
+
},
|
| 645 |
+
"funding": {
|
| 646 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 647 |
+
}
|
| 648 |
+
},
|
| 649 |
+
"node_modules/es-define-property": {
|
| 650 |
+
"version": "1.0.1",
|
| 651 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 652 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 653 |
+
"license": "MIT",
|
| 654 |
+
"engines": {
|
| 655 |
+
"node": ">= 0.4"
|
| 656 |
+
}
|
| 657 |
+
},
|
| 658 |
+
"node_modules/es-errors": {
|
| 659 |
+
"version": "1.3.0",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 661 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 662 |
+
"license": "MIT",
|
| 663 |
+
"engines": {
|
| 664 |
+
"node": ">= 0.4"
|
| 665 |
+
}
|
| 666 |
+
},
|
| 667 |
+
"node_modules/es-object-atoms": {
|
| 668 |
+
"version": "1.1.1",
|
| 669 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 670 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 671 |
+
"license": "MIT",
|
| 672 |
+
"dependencies": {
|
| 673 |
+
"es-errors": "^1.3.0"
|
| 674 |
+
},
|
| 675 |
+
"engines": {
|
| 676 |
+
"node": ">= 0.4"
|
| 677 |
+
}
|
| 678 |
+
},
|
| 679 |
+
"node_modules/escalade": {
|
| 680 |
+
"version": "3.2.0",
|
| 681 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 682 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 683 |
+
"license": "MIT",
|
| 684 |
+
"engines": {
|
| 685 |
+
"node": ">=6"
|
| 686 |
+
}
|
| 687 |
+
},
|
| 688 |
+
"node_modules/escape-html": {
|
| 689 |
+
"version": "1.0.3",
|
| 690 |
+
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
| 691 |
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
| 692 |
+
"license": "MIT"
|
| 693 |
+
},
|
| 694 |
+
"node_modules/etag": {
|
| 695 |
+
"version": "1.8.1",
|
| 696 |
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
| 697 |
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
| 698 |
+
"license": "MIT",
|
| 699 |
+
"engines": {
|
| 700 |
+
"node": ">= 0.6"
|
| 701 |
+
}
|
| 702 |
+
},
|
| 703 |
+
"node_modules/eventsource": {
|
| 704 |
+
"version": "2.0.2",
|
| 705 |
+
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
|
| 706 |
+
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
|
| 707 |
+
"license": "MIT",
|
| 708 |
+
"engines": {
|
| 709 |
+
"node": ">=12.0.0"
|
| 710 |
+
}
|
| 711 |
+
},
|
| 712 |
+
"node_modules/express": {
|
| 713 |
+
"version": "4.22.1",
|
| 714 |
+
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
| 715 |
+
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
| 716 |
+
"license": "MIT",
|
| 717 |
+
"dependencies": {
|
| 718 |
+
"accepts": "~1.3.8",
|
| 719 |
+
"array-flatten": "1.1.1",
|
| 720 |
+
"body-parser": "~1.20.3",
|
| 721 |
+
"content-disposition": "~0.5.4",
|
| 722 |
+
"content-type": "~1.0.4",
|
| 723 |
+
"cookie": "~0.7.1",
|
| 724 |
+
"cookie-signature": "~1.0.6",
|
| 725 |
+
"debug": "2.6.9",
|
| 726 |
+
"depd": "2.0.0",
|
| 727 |
+
"encodeurl": "~2.0.0",
|
| 728 |
+
"escape-html": "~1.0.3",
|
| 729 |
+
"etag": "~1.8.1",
|
| 730 |
+
"finalhandler": "~1.3.1",
|
| 731 |
+
"fresh": "~0.5.2",
|
| 732 |
+
"http-errors": "~2.0.0",
|
| 733 |
+
"merge-descriptors": "1.0.3",
|
| 734 |
+
"methods": "~1.1.2",
|
| 735 |
+
"on-finished": "~2.4.1",
|
| 736 |
+
"parseurl": "~1.3.3",
|
| 737 |
+
"path-to-regexp": "~0.1.12",
|
| 738 |
+
"proxy-addr": "~2.0.7",
|
| 739 |
+
"qs": "~6.14.0",
|
| 740 |
+
"range-parser": "~1.2.1",
|
| 741 |
+
"safe-buffer": "5.2.1",
|
| 742 |
+
"send": "~0.19.0",
|
| 743 |
+
"serve-static": "~1.16.2",
|
| 744 |
+
"setprototypeof": "1.2.0",
|
| 745 |
+
"statuses": "~2.0.1",
|
| 746 |
+
"type-is": "~1.6.18",
|
| 747 |
+
"utils-merge": "1.0.1",
|
| 748 |
+
"vary": "~1.1.2"
|
| 749 |
+
},
|
| 750 |
+
"engines": {
|
| 751 |
+
"node": ">= 0.10.0"
|
| 752 |
+
},
|
| 753 |
+
"funding": {
|
| 754 |
+
"type": "opencollective",
|
| 755 |
+
"url": "https://opencollective.com/express"
|
| 756 |
+
}
|
| 757 |
+
},
|
| 758 |
+
"node_modules/express-rate-limit": {
|
| 759 |
+
"version": "8.3.2",
|
| 760 |
+
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
| 761 |
+
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
| 762 |
+
"license": "MIT",
|
| 763 |
+
"dependencies": {
|
| 764 |
+
"ip-address": "10.1.0"
|
| 765 |
+
},
|
| 766 |
+
"engines": {
|
| 767 |
+
"node": ">= 16"
|
| 768 |
+
},
|
| 769 |
+
"funding": {
|
| 770 |
+
"url": "https://github.com/sponsors/express-rate-limit"
|
| 771 |
+
},
|
| 772 |
+
"peerDependencies": {
|
| 773 |
+
"express": ">= 4.11"
|
| 774 |
+
}
|
| 775 |
+
},
|
| 776 |
+
"node_modules/fast-string-truncated-width": {
|
| 777 |
+
"version": "3.0.3",
|
| 778 |
+
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
|
| 779 |
+
"integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
|
| 780 |
+
"license": "MIT"
|
| 781 |
+
},
|
| 782 |
+
"node_modules/fast-string-width": {
|
| 783 |
+
"version": "3.0.2",
|
| 784 |
+
"resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
|
| 785 |
+
"integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
|
| 786 |
+
"license": "MIT",
|
| 787 |
+
"dependencies": {
|
| 788 |
+
"fast-string-truncated-width": "^3.0.2"
|
| 789 |
+
}
|
| 790 |
+
},
|
| 791 |
+
"node_modules/fast-wrap-ansi": {
|
| 792 |
+
"version": "0.2.0",
|
| 793 |
+
"resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz",
|
| 794 |
+
"integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==",
|
| 795 |
+
"license": "MIT",
|
| 796 |
+
"dependencies": {
|
| 797 |
+
"fast-string-width": "^3.0.2"
|
| 798 |
+
}
|
| 799 |
+
},
|
| 800 |
+
"node_modules/fetch-blob": {
|
| 801 |
+
"version": "3.2.0",
|
| 802 |
+
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
| 803 |
+
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
| 804 |
+
"funding": [
|
| 805 |
+
{
|
| 806 |
+
"type": "github",
|
| 807 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"type": "paypal",
|
| 811 |
+
"url": "https://paypal.me/jimmywarting"
|
| 812 |
+
}
|
| 813 |
+
],
|
| 814 |
+
"license": "MIT",
|
| 815 |
+
"dependencies": {
|
| 816 |
+
"node-domexception": "^1.0.0",
|
| 817 |
+
"web-streams-polyfill": "^3.0.3"
|
| 818 |
+
},
|
| 819 |
+
"engines": {
|
| 820 |
+
"node": "^12.20 || >= 14.13"
|
| 821 |
+
}
|
| 822 |
+
},
|
| 823 |
+
"node_modules/finalhandler": {
|
| 824 |
+
"version": "1.3.2",
|
| 825 |
+
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
| 826 |
+
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
| 827 |
+
"license": "MIT",
|
| 828 |
+
"dependencies": {
|
| 829 |
+
"debug": "2.6.9",
|
| 830 |
+
"encodeurl": "~2.0.0",
|
| 831 |
+
"escape-html": "~1.0.3",
|
| 832 |
+
"on-finished": "~2.4.1",
|
| 833 |
+
"parseurl": "~1.3.3",
|
| 834 |
+
"statuses": "~2.0.2",
|
| 835 |
+
"unpipe": "~1.0.0"
|
| 836 |
+
},
|
| 837 |
+
"engines": {
|
| 838 |
+
"node": ">= 0.8"
|
| 839 |
+
}
|
| 840 |
+
},
|
| 841 |
+
"node_modules/formdata-polyfill": {
|
| 842 |
+
"version": "4.0.10",
|
| 843 |
+
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
| 844 |
+
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
| 845 |
+
"license": "MIT",
|
| 846 |
+
"dependencies": {
|
| 847 |
+
"fetch-blob": "^3.1.2"
|
| 848 |
+
},
|
| 849 |
+
"engines": {
|
| 850 |
+
"node": ">=12.20.0"
|
| 851 |
+
}
|
| 852 |
+
},
|
| 853 |
+
"node_modules/forwarded": {
|
| 854 |
+
"version": "0.2.0",
|
| 855 |
+
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
| 856 |
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
| 857 |
+
"license": "MIT",
|
| 858 |
+
"engines": {
|
| 859 |
+
"node": ">= 0.6"
|
| 860 |
+
}
|
| 861 |
+
},
|
| 862 |
+
"node_modules/fresh": {
|
| 863 |
+
"version": "0.5.2",
|
| 864 |
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
| 865 |
+
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
| 866 |
+
"license": "MIT",
|
| 867 |
+
"engines": {
|
| 868 |
+
"node": ">= 0.6"
|
| 869 |
+
}
|
| 870 |
+
},
|
| 871 |
+
"node_modules/function-bind": {
|
| 872 |
+
"version": "1.1.2",
|
| 873 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 874 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 875 |
+
"license": "MIT",
|
| 876 |
+
"funding": {
|
| 877 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 878 |
+
}
|
| 879 |
+
},
|
| 880 |
+
"node_modules/get-caller-file": {
|
| 881 |
+
"version": "2.0.5",
|
| 882 |
+
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
| 883 |
+
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
| 884 |
+
"license": "ISC",
|
| 885 |
+
"engines": {
|
| 886 |
+
"node": "6.* || 8.* || >= 10.*"
|
| 887 |
+
}
|
| 888 |
+
},
|
| 889 |
+
"node_modules/get-intrinsic": {
|
| 890 |
+
"version": "1.3.0",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 892 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 893 |
+
"license": "MIT",
|
| 894 |
+
"dependencies": {
|
| 895 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 896 |
+
"es-define-property": "^1.0.1",
|
| 897 |
+
"es-errors": "^1.3.0",
|
| 898 |
+
"es-object-atoms": "^1.1.1",
|
| 899 |
+
"function-bind": "^1.1.2",
|
| 900 |
+
"get-proto": "^1.0.1",
|
| 901 |
+
"gopd": "^1.2.0",
|
| 902 |
+
"has-symbols": "^1.1.0",
|
| 903 |
+
"hasown": "^2.0.2",
|
| 904 |
+
"math-intrinsics": "^1.1.0"
|
| 905 |
+
},
|
| 906 |
+
"engines": {
|
| 907 |
+
"node": ">= 0.4"
|
| 908 |
+
},
|
| 909 |
+
"funding": {
|
| 910 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 911 |
+
}
|
| 912 |
+
},
|
| 913 |
+
"node_modules/get-proto": {
|
| 914 |
+
"version": "1.0.1",
|
| 915 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 916 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 917 |
+
"license": "MIT",
|
| 918 |
+
"dependencies": {
|
| 919 |
+
"dunder-proto": "^1.0.1",
|
| 920 |
+
"es-object-atoms": "^1.0.0"
|
| 921 |
+
},
|
| 922 |
+
"engines": {
|
| 923 |
+
"node": ">= 0.4"
|
| 924 |
+
}
|
| 925 |
+
},
|
| 926 |
+
"node_modules/gopd": {
|
| 927 |
+
"version": "1.2.0",
|
| 928 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 929 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 930 |
+
"license": "MIT",
|
| 931 |
+
"engines": {
|
| 932 |
+
"node": ">= 0.4"
|
| 933 |
+
},
|
| 934 |
+
"funding": {
|
| 935 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 936 |
+
}
|
| 937 |
+
},
|
| 938 |
+
"node_modules/graphql": {
|
| 939 |
+
"version": "16.13.2",
|
| 940 |
+
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz",
|
| 941 |
+
"integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==",
|
| 942 |
+
"license": "MIT",
|
| 943 |
+
"engines": {
|
| 944 |
+
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
| 945 |
+
}
|
| 946 |
+
},
|
| 947 |
+
"node_modules/has-symbols": {
|
| 948 |
+
"version": "1.1.0",
|
| 949 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 950 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 951 |
+
"license": "MIT",
|
| 952 |
+
"engines": {
|
| 953 |
+
"node": ">= 0.4"
|
| 954 |
+
},
|
| 955 |
+
"funding": {
|
| 956 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 957 |
+
}
|
| 958 |
+
},
|
| 959 |
+
"node_modules/hasown": {
|
| 960 |
+
"version": "2.0.3",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
| 962 |
+
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
| 963 |
+
"license": "MIT",
|
| 964 |
+
"dependencies": {
|
| 965 |
+
"function-bind": "^1.1.2"
|
| 966 |
+
},
|
| 967 |
+
"engines": {
|
| 968 |
+
"node": ">= 0.4"
|
| 969 |
+
}
|
| 970 |
+
},
|
| 971 |
+
"node_modules/headers-polyfill": {
|
| 972 |
+
"version": "5.0.1",
|
| 973 |
+
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz",
|
| 974 |
+
"integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==",
|
| 975 |
+
"license": "MIT",
|
| 976 |
+
"dependencies": {
|
| 977 |
+
"@types/set-cookie-parser": "^2.4.10",
|
| 978 |
+
"set-cookie-parser": "^3.0.1"
|
| 979 |
+
}
|
| 980 |
+
},
|
| 981 |
+
"node_modules/html-to-text": {
|
| 982 |
+
"version": "9.0.5",
|
| 983 |
+
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
| 984 |
+
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
| 985 |
+
"license": "MIT",
|
| 986 |
+
"dependencies": {
|
| 987 |
+
"@selderee/plugin-htmlparser2": "^0.11.0",
|
| 988 |
+
"deepmerge": "^4.3.1",
|
| 989 |
+
"dom-serializer": "^2.0.0",
|
| 990 |
+
"htmlparser2": "^8.0.2",
|
| 991 |
+
"selderee": "^0.11.0"
|
| 992 |
+
},
|
| 993 |
+
"engines": {
|
| 994 |
+
"node": ">=14"
|
| 995 |
+
}
|
| 996 |
+
},
|
| 997 |
+
"node_modules/htmlparser2": {
|
| 998 |
+
"version": "8.0.2",
|
| 999 |
+
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
| 1000 |
+
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
| 1001 |
+
"funding": [
|
| 1002 |
+
"https://github.com/fb55/htmlparser2?sponsor=1",
|
| 1003 |
+
{
|
| 1004 |
+
"type": "github",
|
| 1005 |
+
"url": "https://github.com/sponsors/fb55"
|
| 1006 |
+
}
|
| 1007 |
+
],
|
| 1008 |
+
"license": "MIT",
|
| 1009 |
+
"dependencies": {
|
| 1010 |
+
"domelementtype": "^2.3.0",
|
| 1011 |
+
"domhandler": "^5.0.3",
|
| 1012 |
+
"domutils": "^3.0.1",
|
| 1013 |
+
"entities": "^4.4.0"
|
| 1014 |
+
}
|
| 1015 |
+
},
|
| 1016 |
+
"node_modules/http-errors": {
|
| 1017 |
+
"version": "2.0.1",
|
| 1018 |
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
| 1019 |
+
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
| 1020 |
+
"license": "MIT",
|
| 1021 |
+
"dependencies": {
|
| 1022 |
+
"depd": "~2.0.0",
|
| 1023 |
+
"inherits": "~2.0.4",
|
| 1024 |
+
"setprototypeof": "~1.2.0",
|
| 1025 |
+
"statuses": "~2.0.2",
|
| 1026 |
+
"toidentifier": "~1.0.1"
|
| 1027 |
+
},
|
| 1028 |
+
"engines": {
|
| 1029 |
+
"node": ">= 0.8"
|
| 1030 |
+
},
|
| 1031 |
+
"funding": {
|
| 1032 |
+
"type": "opencollective",
|
| 1033 |
+
"url": "https://opencollective.com/express"
|
| 1034 |
+
}
|
| 1035 |
+
},
|
| 1036 |
+
"node_modules/iceberg-js": {
|
| 1037 |
+
"version": "0.8.1",
|
| 1038 |
+
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
| 1039 |
+
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
| 1040 |
+
"license": "MIT",
|
| 1041 |
+
"engines": {
|
| 1042 |
+
"node": ">=20.0.0"
|
| 1043 |
+
}
|
| 1044 |
+
},
|
| 1045 |
+
"node_modules/iconv-lite": {
|
| 1046 |
+
"version": "0.4.24",
|
| 1047 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
| 1048 |
+
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
| 1049 |
+
"license": "MIT",
|
| 1050 |
+
"dependencies": {
|
| 1051 |
+
"safer-buffer": ">= 2.1.2 < 3"
|
| 1052 |
+
},
|
| 1053 |
+
"engines": {
|
| 1054 |
+
"node": ">=0.10.0"
|
| 1055 |
+
}
|
| 1056 |
+
},
|
| 1057 |
+
"node_modules/inherits": {
|
| 1058 |
+
"version": "2.0.4",
|
| 1059 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 1060 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 1061 |
+
"license": "ISC"
|
| 1062 |
+
},
|
| 1063 |
+
"node_modules/ip-address": {
|
| 1064 |
+
"version": "10.1.0",
|
| 1065 |
+
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
| 1066 |
+
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
| 1067 |
+
"license": "MIT",
|
| 1068 |
+
"engines": {
|
| 1069 |
+
"node": ">= 12"
|
| 1070 |
+
}
|
| 1071 |
+
},
|
| 1072 |
+
"node_modules/ipaddr.js": {
|
| 1073 |
+
"version": "1.9.1",
|
| 1074 |
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
| 1075 |
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
| 1076 |
+
"license": "MIT",
|
| 1077 |
+
"engines": {
|
| 1078 |
+
"node": ">= 0.10"
|
| 1079 |
+
}
|
| 1080 |
+
},
|
| 1081 |
+
"node_modules/is-fullwidth-code-point": {
|
| 1082 |
+
"version": "3.0.0",
|
| 1083 |
+
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
| 1084 |
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
| 1085 |
+
"license": "MIT",
|
| 1086 |
+
"engines": {
|
| 1087 |
+
"node": ">=8"
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
+
"node_modules/is-node-process": {
|
| 1091 |
+
"version": "1.2.0",
|
| 1092 |
+
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
|
| 1093 |
+
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
|
| 1094 |
+
"license": "MIT"
|
| 1095 |
+
},
|
| 1096 |
+
"node_modules/leac": {
|
| 1097 |
+
"version": "0.6.0",
|
| 1098 |
+
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
| 1099 |
+
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
| 1100 |
+
"license": "MIT",
|
| 1101 |
+
"funding": {
|
| 1102 |
+
"url": "https://ko-fi.com/killymxi"
|
| 1103 |
+
}
|
| 1104 |
+
},
|
| 1105 |
+
"node_modules/math-intrinsics": {
|
| 1106 |
+
"version": "1.1.0",
|
| 1107 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 1108 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 1109 |
+
"license": "MIT",
|
| 1110 |
+
"engines": {
|
| 1111 |
+
"node": ">= 0.4"
|
| 1112 |
+
}
|
| 1113 |
+
},
|
| 1114 |
+
"node_modules/media-typer": {
|
| 1115 |
+
"version": "0.3.0",
|
| 1116 |
+
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
| 1117 |
+
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
| 1118 |
+
"license": "MIT",
|
| 1119 |
+
"engines": {
|
| 1120 |
+
"node": ">= 0.6"
|
| 1121 |
+
}
|
| 1122 |
+
},
|
| 1123 |
+
"node_modules/merge-descriptors": {
|
| 1124 |
+
"version": "1.0.3",
|
| 1125 |
+
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
| 1126 |
+
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
| 1127 |
+
"license": "MIT",
|
| 1128 |
+
"funding": {
|
| 1129 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1130 |
+
}
|
| 1131 |
+
},
|
| 1132 |
+
"node_modules/methods": {
|
| 1133 |
+
"version": "1.1.2",
|
| 1134 |
+
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
| 1135 |
+
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
| 1136 |
+
"license": "MIT",
|
| 1137 |
+
"engines": {
|
| 1138 |
+
"node": ">= 0.6"
|
| 1139 |
+
}
|
| 1140 |
+
},
|
| 1141 |
+
"node_modules/mime": {
|
| 1142 |
+
"version": "1.6.0",
|
| 1143 |
+
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
| 1144 |
+
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
| 1145 |
+
"license": "MIT",
|
| 1146 |
+
"bin": {
|
| 1147 |
+
"mime": "cli.js"
|
| 1148 |
+
},
|
| 1149 |
+
"engines": {
|
| 1150 |
+
"node": ">=4"
|
| 1151 |
+
}
|
| 1152 |
+
},
|
| 1153 |
+
"node_modules/mime-db": {
|
| 1154 |
+
"version": "1.52.0",
|
| 1155 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 1156 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 1157 |
+
"license": "MIT",
|
| 1158 |
+
"engines": {
|
| 1159 |
+
"node": ">= 0.6"
|
| 1160 |
+
}
|
| 1161 |
+
},
|
| 1162 |
+
"node_modules/mime-types": {
|
| 1163 |
+
"version": "2.1.35",
|
| 1164 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 1165 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 1166 |
+
"license": "MIT",
|
| 1167 |
+
"dependencies": {
|
| 1168 |
+
"mime-db": "1.52.0"
|
| 1169 |
+
},
|
| 1170 |
+
"engines": {
|
| 1171 |
+
"node": ">= 0.6"
|
| 1172 |
+
}
|
| 1173 |
+
},
|
| 1174 |
+
"node_modules/ms": {
|
| 1175 |
+
"version": "2.0.0",
|
| 1176 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
| 1177 |
+
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
| 1178 |
+
"license": "MIT"
|
| 1179 |
+
},
|
| 1180 |
+
"node_modules/msw": {
|
| 1181 |
+
"version": "2.13.4",
|
| 1182 |
+
"resolved": "https://registry.npmjs.org/msw/-/msw-2.13.4.tgz",
|
| 1183 |
+
"integrity": "sha512-fPlKBeFe+8rpcyR3umUmmHuNwu6gc6T3STvkgEa9WDX/HEgal9wDeflpCUAIRtmvaLZM2igfI5y1bZ9G5J26KA==",
|
| 1184 |
+
"hasInstallScript": true,
|
| 1185 |
+
"license": "MIT",
|
| 1186 |
+
"dependencies": {
|
| 1187 |
+
"@inquirer/confirm": "^6.0.11",
|
| 1188 |
+
"@mswjs/interceptors": "^0.41.3",
|
| 1189 |
+
"@open-draft/deferred-promise": "^3.0.0",
|
| 1190 |
+
"@types/statuses": "^2.0.6",
|
| 1191 |
+
"cookie": "^1.1.1",
|
| 1192 |
+
"graphql": "^16.13.2",
|
| 1193 |
+
"headers-polyfill": "^5.0.1",
|
| 1194 |
+
"is-node-process": "^1.2.0",
|
| 1195 |
+
"outvariant": "^1.4.3",
|
| 1196 |
+
"path-to-regexp": "^6.3.0",
|
| 1197 |
+
"picocolors": "^1.1.1",
|
| 1198 |
+
"rettime": "^0.11.7",
|
| 1199 |
+
"statuses": "^2.0.2",
|
| 1200 |
+
"strict-event-emitter": "^0.5.1",
|
| 1201 |
+
"tough-cookie": "^6.0.1",
|
| 1202 |
+
"type-fest": "^5.5.0",
|
| 1203 |
+
"until-async": "^3.0.2",
|
| 1204 |
+
"yargs": "^17.7.2"
|
| 1205 |
+
},
|
| 1206 |
+
"bin": {
|
| 1207 |
+
"msw": "cli/index.js"
|
| 1208 |
+
},
|
| 1209 |
+
"engines": {
|
| 1210 |
+
"node": ">=18"
|
| 1211 |
+
},
|
| 1212 |
+
"funding": {
|
| 1213 |
+
"url": "https://github.com/sponsors/mswjs"
|
| 1214 |
+
},
|
| 1215 |
+
"peerDependencies": {
|
| 1216 |
+
"typescript": ">= 4.8.x"
|
| 1217 |
+
},
|
| 1218 |
+
"peerDependenciesMeta": {
|
| 1219 |
+
"typescript": {
|
| 1220 |
+
"optional": true
|
| 1221 |
+
}
|
| 1222 |
+
}
|
| 1223 |
+
},
|
| 1224 |
+
"node_modules/msw/node_modules/cookie": {
|
| 1225 |
+
"version": "1.1.1",
|
| 1226 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
| 1227 |
+
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
| 1228 |
+
"license": "MIT",
|
| 1229 |
+
"engines": {
|
| 1230 |
+
"node": ">=18"
|
| 1231 |
+
},
|
| 1232 |
+
"funding": {
|
| 1233 |
+
"type": "opencollective",
|
| 1234 |
+
"url": "https://opencollective.com/express"
|
| 1235 |
+
}
|
| 1236 |
+
},
|
| 1237 |
+
"node_modules/msw/node_modules/path-to-regexp": {
|
| 1238 |
+
"version": "6.3.0",
|
| 1239 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
| 1240 |
+
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
| 1241 |
+
"license": "MIT"
|
| 1242 |
+
},
|
| 1243 |
+
"node_modules/mute-stream": {
|
| 1244 |
+
"version": "3.0.0",
|
| 1245 |
+
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
|
| 1246 |
+
"integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
|
| 1247 |
+
"license": "ISC",
|
| 1248 |
+
"engines": {
|
| 1249 |
+
"node": "^20.17.0 || >=22.9.0"
|
| 1250 |
+
}
|
| 1251 |
+
},
|
| 1252 |
+
"node_modules/negotiator": {
|
| 1253 |
+
"version": "0.6.3",
|
| 1254 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
| 1255 |
+
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
| 1256 |
+
"license": "MIT",
|
| 1257 |
+
"engines": {
|
| 1258 |
+
"node": ">= 0.6"
|
| 1259 |
+
}
|
| 1260 |
+
},
|
| 1261 |
+
"node_modules/node-domexception": {
|
| 1262 |
+
"version": "1.0.0",
|
| 1263 |
+
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
| 1264 |
+
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
| 1265 |
+
"deprecated": "Use your platform's native DOMException instead",
|
| 1266 |
+
"funding": [
|
| 1267 |
+
{
|
| 1268 |
+
"type": "github",
|
| 1269 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 1270 |
+
},
|
| 1271 |
+
{
|
| 1272 |
+
"type": "github",
|
| 1273 |
+
"url": "https://paypal.me/jimmywarting"
|
| 1274 |
+
}
|
| 1275 |
+
],
|
| 1276 |
+
"license": "MIT",
|
| 1277 |
+
"engines": {
|
| 1278 |
+
"node": ">=10.5.0"
|
| 1279 |
+
}
|
| 1280 |
+
},
|
| 1281 |
+
"node_modules/node-fetch": {
|
| 1282 |
+
"version": "3.3.2",
|
| 1283 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
| 1284 |
+
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
| 1285 |
+
"license": "MIT",
|
| 1286 |
+
"dependencies": {
|
| 1287 |
+
"data-uri-to-buffer": "^4.0.0",
|
| 1288 |
+
"fetch-blob": "^3.1.4",
|
| 1289 |
+
"formdata-polyfill": "^4.0.10"
|
| 1290 |
+
},
|
| 1291 |
+
"engines": {
|
| 1292 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 1293 |
+
},
|
| 1294 |
+
"funding": {
|
| 1295 |
+
"type": "opencollective",
|
| 1296 |
+
"url": "https://opencollective.com/node-fetch"
|
| 1297 |
+
}
|
| 1298 |
+
},
|
| 1299 |
+
"node_modules/node-gyp-build": {
|
| 1300 |
+
"version": "4.8.4",
|
| 1301 |
+
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
| 1302 |
+
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
| 1303 |
+
"license": "MIT",
|
| 1304 |
+
"bin": {
|
| 1305 |
+
"node-gyp-build": "bin.js",
|
| 1306 |
+
"node-gyp-build-optional": "optional.js",
|
| 1307 |
+
"node-gyp-build-test": "build-test.js"
|
| 1308 |
+
}
|
| 1309 |
+
},
|
| 1310 |
+
"node_modules/object-inspect": {
|
| 1311 |
+
"version": "1.13.4",
|
| 1312 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
| 1313 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
| 1314 |
+
"license": "MIT",
|
| 1315 |
+
"engines": {
|
| 1316 |
+
"node": ">= 0.4"
|
| 1317 |
+
},
|
| 1318 |
+
"funding": {
|
| 1319 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1320 |
+
}
|
| 1321 |
+
},
|
| 1322 |
+
"node_modules/on-finished": {
|
| 1323 |
+
"version": "2.4.1",
|
| 1324 |
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
| 1325 |
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
| 1326 |
+
"license": "MIT",
|
| 1327 |
+
"dependencies": {
|
| 1328 |
+
"ee-first": "1.1.1"
|
| 1329 |
+
},
|
| 1330 |
+
"engines": {
|
| 1331 |
+
"node": ">= 0.8"
|
| 1332 |
+
}
|
| 1333 |
+
},
|
| 1334 |
+
"node_modules/openai": {
|
| 1335 |
+
"version": "6.34.0",
|
| 1336 |
+
"resolved": "https://registry.npmjs.org/openai/-/openai-6.34.0.tgz",
|
| 1337 |
+
"integrity": "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==",
|
| 1338 |
+
"license": "Apache-2.0",
|
| 1339 |
+
"bin": {
|
| 1340 |
+
"openai": "bin/cli"
|
| 1341 |
+
},
|
| 1342 |
+
"peerDependencies": {
|
| 1343 |
+
"ws": "^8.18.0",
|
| 1344 |
+
"zod": "^3.25 || ^4.0"
|
| 1345 |
+
},
|
| 1346 |
+
"peerDependenciesMeta": {
|
| 1347 |
+
"ws": {
|
| 1348 |
+
"optional": true
|
| 1349 |
+
},
|
| 1350 |
+
"zod": {
|
| 1351 |
+
"optional": true
|
| 1352 |
+
}
|
| 1353 |
+
}
|
| 1354 |
+
},
|
| 1355 |
+
"node_modules/outvariant": {
|
| 1356 |
+
"version": "1.4.3",
|
| 1357 |
+
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
|
| 1358 |
+
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
|
| 1359 |
+
"license": "MIT"
|
| 1360 |
+
},
|
| 1361 |
+
"node_modules/parseley": {
|
| 1362 |
+
"version": "0.12.1",
|
| 1363 |
+
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
| 1364 |
+
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
| 1365 |
+
"license": "MIT",
|
| 1366 |
+
"dependencies": {
|
| 1367 |
+
"leac": "^0.6.0",
|
| 1368 |
+
"peberminta": "^0.9.0"
|
| 1369 |
+
},
|
| 1370 |
+
"funding": {
|
| 1371 |
+
"url": "https://ko-fi.com/killymxi"
|
| 1372 |
+
}
|
| 1373 |
+
},
|
| 1374 |
+
"node_modules/parseurl": {
|
| 1375 |
+
"version": "1.3.3",
|
| 1376 |
+
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
| 1377 |
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
| 1378 |
+
"license": "MIT",
|
| 1379 |
+
"engines": {
|
| 1380 |
+
"node": ">= 0.8"
|
| 1381 |
+
}
|
| 1382 |
+
},
|
| 1383 |
+
"node_modules/path-to-regexp": {
|
| 1384 |
+
"version": "0.1.13",
|
| 1385 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
| 1386 |
+
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
| 1387 |
+
"license": "MIT"
|
| 1388 |
+
},
|
| 1389 |
+
"node_modules/peberminta": {
|
| 1390 |
+
"version": "0.9.0",
|
| 1391 |
+
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
| 1392 |
+
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
| 1393 |
+
"license": "MIT",
|
| 1394 |
+
"funding": {
|
| 1395 |
+
"url": "https://ko-fi.com/killymxi"
|
| 1396 |
+
}
|
| 1397 |
+
},
|
| 1398 |
+
"node_modules/pg": {
|
| 1399 |
+
"version": "8.20.0",
|
| 1400 |
+
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
| 1401 |
+
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
| 1402 |
+
"license": "MIT",
|
| 1403 |
+
"dependencies": {
|
| 1404 |
+
"pg-connection-string": "^2.12.0",
|
| 1405 |
+
"pg-pool": "^3.13.0",
|
| 1406 |
+
"pg-protocol": "^1.13.0",
|
| 1407 |
+
"pg-types": "2.2.0",
|
| 1408 |
+
"pgpass": "1.0.5"
|
| 1409 |
+
},
|
| 1410 |
+
"engines": {
|
| 1411 |
+
"node": ">= 16.0.0"
|
| 1412 |
+
},
|
| 1413 |
+
"optionalDependencies": {
|
| 1414 |
+
"pg-cloudflare": "^1.3.0"
|
| 1415 |
+
},
|
| 1416 |
+
"peerDependencies": {
|
| 1417 |
+
"pg-native": ">=3.0.1"
|
| 1418 |
+
},
|
| 1419 |
+
"peerDependenciesMeta": {
|
| 1420 |
+
"pg-native": {
|
| 1421 |
+
"optional": true
|
| 1422 |
+
}
|
| 1423 |
+
}
|
| 1424 |
+
},
|
| 1425 |
+
"node_modules/pg-cloudflare": {
|
| 1426 |
+
"version": "1.3.0",
|
| 1427 |
+
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
| 1428 |
+
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
| 1429 |
+
"license": "MIT",
|
| 1430 |
+
"optional": true
|
| 1431 |
+
},
|
| 1432 |
+
"node_modules/pg-connection-string": {
|
| 1433 |
+
"version": "2.12.0",
|
| 1434 |
+
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
|
| 1435 |
+
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
|
| 1436 |
+
"license": "MIT"
|
| 1437 |
+
},
|
| 1438 |
+
"node_modules/pg-int8": {
|
| 1439 |
+
"version": "1.0.1",
|
| 1440 |
+
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
| 1441 |
+
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
| 1442 |
+
"license": "ISC",
|
| 1443 |
+
"engines": {
|
| 1444 |
+
"node": ">=4.0.0"
|
| 1445 |
+
}
|
| 1446 |
+
},
|
| 1447 |
+
"node_modules/pg-pool": {
|
| 1448 |
+
"version": "3.13.0",
|
| 1449 |
+
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
|
| 1450 |
+
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
|
| 1451 |
+
"license": "MIT",
|
| 1452 |
+
"peerDependencies": {
|
| 1453 |
+
"pg": ">=8.0"
|
| 1454 |
+
}
|
| 1455 |
+
},
|
| 1456 |
+
"node_modules/pg-protocol": {
|
| 1457 |
+
"version": "1.13.0",
|
| 1458 |
+
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
|
| 1459 |
+
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
|
| 1460 |
+
"license": "MIT"
|
| 1461 |
+
},
|
| 1462 |
+
"node_modules/pg-types": {
|
| 1463 |
+
"version": "2.2.0",
|
| 1464 |
+
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
| 1465 |
+
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
| 1466 |
+
"license": "MIT",
|
| 1467 |
+
"dependencies": {
|
| 1468 |
+
"pg-int8": "1.0.1",
|
| 1469 |
+
"postgres-array": "~2.0.0",
|
| 1470 |
+
"postgres-bytea": "~1.0.0",
|
| 1471 |
+
"postgres-date": "~1.0.4",
|
| 1472 |
+
"postgres-interval": "^1.1.0"
|
| 1473 |
+
},
|
| 1474 |
+
"engines": {
|
| 1475 |
+
"node": ">=4"
|
| 1476 |
+
}
|
| 1477 |
+
},
|
| 1478 |
+
"node_modules/pgpass": {
|
| 1479 |
+
"version": "1.0.5",
|
| 1480 |
+
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
| 1481 |
+
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
| 1482 |
+
"license": "MIT",
|
| 1483 |
+
"dependencies": {
|
| 1484 |
+
"split2": "^4.1.0"
|
| 1485 |
+
}
|
| 1486 |
+
},
|
| 1487 |
+
"node_modules/picocolors": {
|
| 1488 |
+
"version": "1.1.1",
|
| 1489 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1490 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1491 |
+
"license": "ISC"
|
| 1492 |
+
},
|
| 1493 |
+
"node_modules/postgres-array": {
|
| 1494 |
+
"version": "2.0.0",
|
| 1495 |
+
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
| 1496 |
+
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
| 1497 |
+
"license": "MIT",
|
| 1498 |
+
"engines": {
|
| 1499 |
+
"node": ">=4"
|
| 1500 |
+
}
|
| 1501 |
+
},
|
| 1502 |
+
"node_modules/postgres-bytea": {
|
| 1503 |
+
"version": "1.0.1",
|
| 1504 |
+
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
| 1505 |
+
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
| 1506 |
+
"license": "MIT",
|
| 1507 |
+
"engines": {
|
| 1508 |
+
"node": ">=0.10.0"
|
| 1509 |
+
}
|
| 1510 |
+
},
|
| 1511 |
+
"node_modules/postgres-date": {
|
| 1512 |
+
"version": "1.0.7",
|
| 1513 |
+
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
| 1514 |
+
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
| 1515 |
+
"license": "MIT",
|
| 1516 |
+
"engines": {
|
| 1517 |
+
"node": ">=0.10.0"
|
| 1518 |
+
}
|
| 1519 |
+
},
|
| 1520 |
+
"node_modules/postgres-interval": {
|
| 1521 |
+
"version": "1.2.0",
|
| 1522 |
+
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
| 1523 |
+
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
| 1524 |
+
"license": "MIT",
|
| 1525 |
+
"dependencies": {
|
| 1526 |
+
"xtend": "^4.0.0"
|
| 1527 |
+
},
|
| 1528 |
+
"engines": {
|
| 1529 |
+
"node": ">=0.10.0"
|
| 1530 |
+
}
|
| 1531 |
+
},
|
| 1532 |
+
"node_modules/proxy-addr": {
|
| 1533 |
+
"version": "2.0.7",
|
| 1534 |
+
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
| 1535 |
+
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
| 1536 |
+
"license": "MIT",
|
| 1537 |
+
"dependencies": {
|
| 1538 |
+
"forwarded": "0.2.0",
|
| 1539 |
+
"ipaddr.js": "1.9.1"
|
| 1540 |
+
},
|
| 1541 |
+
"engines": {
|
| 1542 |
+
"node": ">= 0.10"
|
| 1543 |
+
}
|
| 1544 |
+
},
|
| 1545 |
+
"node_modules/qs": {
|
| 1546 |
+
"version": "6.14.2",
|
| 1547 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
| 1548 |
+
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
| 1549 |
+
"license": "BSD-3-Clause",
|
| 1550 |
+
"dependencies": {
|
| 1551 |
+
"side-channel": "^1.1.0"
|
| 1552 |
+
},
|
| 1553 |
+
"engines": {
|
| 1554 |
+
"node": ">=0.6"
|
| 1555 |
+
},
|
| 1556 |
+
"funding": {
|
| 1557 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1558 |
+
}
|
| 1559 |
+
},
|
| 1560 |
+
"node_modules/range-parser": {
|
| 1561 |
+
"version": "1.2.1",
|
| 1562 |
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
| 1563 |
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
| 1564 |
+
"license": "MIT",
|
| 1565 |
+
"engines": {
|
| 1566 |
+
"node": ">= 0.6"
|
| 1567 |
+
}
|
| 1568 |
+
},
|
| 1569 |
+
"node_modules/raw-body": {
|
| 1570 |
+
"version": "2.5.3",
|
| 1571 |
+
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
| 1572 |
+
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
| 1573 |
+
"license": "MIT",
|
| 1574 |
+
"dependencies": {
|
| 1575 |
+
"bytes": "~3.1.2",
|
| 1576 |
+
"http-errors": "~2.0.1",
|
| 1577 |
+
"iconv-lite": "~0.4.24",
|
| 1578 |
+
"unpipe": "~1.0.0"
|
| 1579 |
+
},
|
| 1580 |
+
"engines": {
|
| 1581 |
+
"node": ">= 0.8"
|
| 1582 |
+
}
|
| 1583 |
+
},
|
| 1584 |
+
"node_modules/require-directory": {
|
| 1585 |
+
"version": "2.1.1",
|
| 1586 |
+
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
| 1587 |
+
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
| 1588 |
+
"license": "MIT",
|
| 1589 |
+
"engines": {
|
| 1590 |
+
"node": ">=0.10.0"
|
| 1591 |
+
}
|
| 1592 |
+
},
|
| 1593 |
+
"node_modules/rettime": {
|
| 1594 |
+
"version": "0.11.8",
|
| 1595 |
+
"resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.8.tgz",
|
| 1596 |
+
"integrity": "sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg==",
|
| 1597 |
+
"license": "MIT"
|
| 1598 |
+
},
|
| 1599 |
+
"node_modules/safe-buffer": {
|
| 1600 |
+
"version": "5.2.1",
|
| 1601 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 1602 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 1603 |
+
"funding": [
|
| 1604 |
+
{
|
| 1605 |
+
"type": "github",
|
| 1606 |
+
"url": "https://github.com/sponsors/feross"
|
| 1607 |
+
},
|
| 1608 |
+
{
|
| 1609 |
+
"type": "patreon",
|
| 1610 |
+
"url": "https://www.patreon.com/feross"
|
| 1611 |
+
},
|
| 1612 |
+
{
|
| 1613 |
+
"type": "consulting",
|
| 1614 |
+
"url": "https://feross.org/support"
|
| 1615 |
+
}
|
| 1616 |
+
],
|
| 1617 |
+
"license": "MIT"
|
| 1618 |
+
},
|
| 1619 |
+
"node_modules/safer-buffer": {
|
| 1620 |
+
"version": "2.1.2",
|
| 1621 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 1622 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 1623 |
+
"license": "MIT"
|
| 1624 |
+
},
|
| 1625 |
+
"node_modules/selderee": {
|
| 1626 |
+
"version": "0.11.0",
|
| 1627 |
+
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
| 1628 |
+
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
| 1629 |
+
"license": "MIT",
|
| 1630 |
+
"dependencies": {
|
| 1631 |
+
"parseley": "^0.12.0"
|
| 1632 |
+
},
|
| 1633 |
+
"funding": {
|
| 1634 |
+
"url": "https://ko-fi.com/killymxi"
|
| 1635 |
+
}
|
| 1636 |
+
},
|
| 1637 |
+
"node_modules/semiver": {
|
| 1638 |
+
"version": "1.1.0",
|
| 1639 |
+
"resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz",
|
| 1640 |
+
"integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==",
|
| 1641 |
+
"license": "MIT",
|
| 1642 |
+
"engines": {
|
| 1643 |
+
"node": ">=6"
|
| 1644 |
+
}
|
| 1645 |
+
},
|
| 1646 |
+
"node_modules/send": {
|
| 1647 |
+
"version": "0.19.2",
|
| 1648 |
+
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
| 1649 |
+
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
| 1650 |
+
"license": "MIT",
|
| 1651 |
+
"dependencies": {
|
| 1652 |
+
"debug": "2.6.9",
|
| 1653 |
+
"depd": "2.0.0",
|
| 1654 |
+
"destroy": "1.2.0",
|
| 1655 |
+
"encodeurl": "~2.0.0",
|
| 1656 |
+
"escape-html": "~1.0.3",
|
| 1657 |
+
"etag": "~1.8.1",
|
| 1658 |
+
"fresh": "~0.5.2",
|
| 1659 |
+
"http-errors": "~2.0.1",
|
| 1660 |
+
"mime": "1.6.0",
|
| 1661 |
+
"ms": "2.1.3",
|
| 1662 |
+
"on-finished": "~2.4.1",
|
| 1663 |
+
"range-parser": "~1.2.1",
|
| 1664 |
+
"statuses": "~2.0.2"
|
| 1665 |
+
},
|
| 1666 |
+
"engines": {
|
| 1667 |
+
"node": ">= 0.8.0"
|
| 1668 |
+
}
|
| 1669 |
+
},
|
| 1670 |
+
"node_modules/send/node_modules/ms": {
|
| 1671 |
+
"version": "2.1.3",
|
| 1672 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1673 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1674 |
+
"license": "MIT"
|
| 1675 |
+
},
|
| 1676 |
+
"node_modules/serve-static": {
|
| 1677 |
+
"version": "1.16.3",
|
| 1678 |
+
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
| 1679 |
+
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
| 1680 |
+
"license": "MIT",
|
| 1681 |
+
"dependencies": {
|
| 1682 |
+
"encodeurl": "~2.0.0",
|
| 1683 |
+
"escape-html": "~1.0.3",
|
| 1684 |
+
"parseurl": "~1.3.3",
|
| 1685 |
+
"send": "~0.19.1"
|
| 1686 |
+
},
|
| 1687 |
+
"engines": {
|
| 1688 |
+
"node": ">= 0.8.0"
|
| 1689 |
+
}
|
| 1690 |
+
},
|
| 1691 |
+
"node_modules/set-cookie-parser": {
|
| 1692 |
+
"version": "3.1.0",
|
| 1693 |
+
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
|
| 1694 |
+
"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
|
| 1695 |
+
"license": "MIT"
|
| 1696 |
+
},
|
| 1697 |
+
"node_modules/setprototypeof": {
|
| 1698 |
+
"version": "1.2.0",
|
| 1699 |
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
| 1700 |
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
| 1701 |
+
"license": "ISC"
|
| 1702 |
+
},
|
| 1703 |
+
"node_modules/side-channel": {
|
| 1704 |
+
"version": "1.1.0",
|
| 1705 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
| 1706 |
+
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
| 1707 |
+
"license": "MIT",
|
| 1708 |
+
"dependencies": {
|
| 1709 |
+
"es-errors": "^1.3.0",
|
| 1710 |
+
"object-inspect": "^1.13.3",
|
| 1711 |
+
"side-channel-list": "^1.0.0",
|
| 1712 |
+
"side-channel-map": "^1.0.1",
|
| 1713 |
+
"side-channel-weakmap": "^1.0.2"
|
| 1714 |
+
},
|
| 1715 |
+
"engines": {
|
| 1716 |
+
"node": ">= 0.4"
|
| 1717 |
+
},
|
| 1718 |
+
"funding": {
|
| 1719 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1720 |
+
}
|
| 1721 |
+
},
|
| 1722 |
+
"node_modules/side-channel-list": {
|
| 1723 |
+
"version": "1.0.1",
|
| 1724 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
| 1725 |
+
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
| 1726 |
+
"license": "MIT",
|
| 1727 |
+
"dependencies": {
|
| 1728 |
+
"es-errors": "^1.3.0",
|
| 1729 |
+
"object-inspect": "^1.13.4"
|
| 1730 |
+
},
|
| 1731 |
+
"engines": {
|
| 1732 |
+
"node": ">= 0.4"
|
| 1733 |
+
},
|
| 1734 |
+
"funding": {
|
| 1735 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1736 |
+
}
|
| 1737 |
+
},
|
| 1738 |
+
"node_modules/side-channel-map": {
|
| 1739 |
+
"version": "1.0.1",
|
| 1740 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
| 1741 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
| 1742 |
+
"license": "MIT",
|
| 1743 |
+
"dependencies": {
|
| 1744 |
+
"call-bound": "^1.0.2",
|
| 1745 |
+
"es-errors": "^1.3.0",
|
| 1746 |
+
"get-intrinsic": "^1.2.5",
|
| 1747 |
+
"object-inspect": "^1.13.3"
|
| 1748 |
+
},
|
| 1749 |
+
"engines": {
|
| 1750 |
+
"node": ">= 0.4"
|
| 1751 |
+
},
|
| 1752 |
+
"funding": {
|
| 1753 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1754 |
+
}
|
| 1755 |
+
},
|
| 1756 |
+
"node_modules/side-channel-weakmap": {
|
| 1757 |
+
"version": "1.0.2",
|
| 1758 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
| 1759 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
| 1760 |
+
"license": "MIT",
|
| 1761 |
+
"dependencies": {
|
| 1762 |
+
"call-bound": "^1.0.2",
|
| 1763 |
+
"es-errors": "^1.3.0",
|
| 1764 |
+
"get-intrinsic": "^1.2.5",
|
| 1765 |
+
"object-inspect": "^1.13.3",
|
| 1766 |
+
"side-channel-map": "^1.0.1"
|
| 1767 |
+
},
|
| 1768 |
+
"engines": {
|
| 1769 |
+
"node": ">= 0.4"
|
| 1770 |
+
},
|
| 1771 |
+
"funding": {
|
| 1772 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1773 |
+
}
|
| 1774 |
+
},
|
| 1775 |
+
"node_modules/signal-exit": {
|
| 1776 |
+
"version": "4.1.0",
|
| 1777 |
+
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
| 1778 |
+
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
| 1779 |
+
"license": "ISC",
|
| 1780 |
+
"engines": {
|
| 1781 |
+
"node": ">=14"
|
| 1782 |
+
},
|
| 1783 |
+
"funding": {
|
| 1784 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 1785 |
+
}
|
| 1786 |
+
},
|
| 1787 |
+
"node_modules/split2": {
|
| 1788 |
+
"version": "4.2.0",
|
| 1789 |
+
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
| 1790 |
+
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
| 1791 |
+
"license": "ISC",
|
| 1792 |
+
"engines": {
|
| 1793 |
+
"node": ">= 10.x"
|
| 1794 |
+
}
|
| 1795 |
+
},
|
| 1796 |
+
"node_modules/statuses": {
|
| 1797 |
+
"version": "2.0.2",
|
| 1798 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
| 1799 |
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
| 1800 |
+
"license": "MIT",
|
| 1801 |
+
"engines": {
|
| 1802 |
+
"node": ">= 0.8"
|
| 1803 |
+
}
|
| 1804 |
+
},
|
| 1805 |
+
"node_modules/strict-event-emitter": {
|
| 1806 |
+
"version": "0.5.1",
|
| 1807 |
+
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
|
| 1808 |
+
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
|
| 1809 |
+
"license": "MIT"
|
| 1810 |
+
},
|
| 1811 |
+
"node_modules/string-width": {
|
| 1812 |
+
"version": "4.2.3",
|
| 1813 |
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
| 1814 |
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
| 1815 |
+
"license": "MIT",
|
| 1816 |
+
"dependencies": {
|
| 1817 |
+
"emoji-regex": "^8.0.0",
|
| 1818 |
+
"is-fullwidth-code-point": "^3.0.0",
|
| 1819 |
+
"strip-ansi": "^6.0.1"
|
| 1820 |
+
},
|
| 1821 |
+
"engines": {
|
| 1822 |
+
"node": ">=8"
|
| 1823 |
+
}
|
| 1824 |
+
},
|
| 1825 |
+
"node_modules/strip-ansi": {
|
| 1826 |
+
"version": "6.0.1",
|
| 1827 |
+
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
| 1828 |
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
| 1829 |
+
"license": "MIT",
|
| 1830 |
+
"dependencies": {
|
| 1831 |
+
"ansi-regex": "^5.0.1"
|
| 1832 |
+
},
|
| 1833 |
+
"engines": {
|
| 1834 |
+
"node": ">=8"
|
| 1835 |
+
}
|
| 1836 |
+
},
|
| 1837 |
+
"node_modules/tagged-tag": {
|
| 1838 |
+
"version": "1.0.0",
|
| 1839 |
+
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
| 1840 |
+
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
| 1841 |
+
"license": "MIT",
|
| 1842 |
+
"engines": {
|
| 1843 |
+
"node": ">=20"
|
| 1844 |
+
},
|
| 1845 |
+
"funding": {
|
| 1846 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1847 |
+
}
|
| 1848 |
+
},
|
| 1849 |
+
"node_modules/tiktoken": {
|
| 1850 |
+
"version": "1.0.22",
|
| 1851 |
+
"resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.22.tgz",
|
| 1852 |
+
"integrity": "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==",
|
| 1853 |
+
"license": "MIT"
|
| 1854 |
+
},
|
| 1855 |
+
"node_modules/tldts": {
|
| 1856 |
+
"version": "7.0.28",
|
| 1857 |
+
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
|
| 1858 |
+
"integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==",
|
| 1859 |
+
"license": "MIT",
|
| 1860 |
+
"dependencies": {
|
| 1861 |
+
"tldts-core": "^7.0.28"
|
| 1862 |
+
},
|
| 1863 |
+
"bin": {
|
| 1864 |
+
"tldts": "bin/cli.js"
|
| 1865 |
+
}
|
| 1866 |
+
},
|
| 1867 |
+
"node_modules/tldts-core": {
|
| 1868 |
+
"version": "7.0.28",
|
| 1869 |
+
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz",
|
| 1870 |
+
"integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==",
|
| 1871 |
+
"license": "MIT"
|
| 1872 |
+
},
|
| 1873 |
+
"node_modules/toidentifier": {
|
| 1874 |
+
"version": "1.0.1",
|
| 1875 |
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
| 1876 |
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
| 1877 |
+
"license": "MIT",
|
| 1878 |
+
"engines": {
|
| 1879 |
+
"node": ">=0.6"
|
| 1880 |
+
}
|
| 1881 |
+
},
|
| 1882 |
+
"node_modules/tough-cookie": {
|
| 1883 |
+
"version": "6.0.1",
|
| 1884 |
+
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
| 1885 |
+
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
| 1886 |
+
"license": "BSD-3-Clause",
|
| 1887 |
+
"dependencies": {
|
| 1888 |
+
"tldts": "^7.0.5"
|
| 1889 |
+
},
|
| 1890 |
+
"engines": {
|
| 1891 |
+
"node": ">=16"
|
| 1892 |
+
}
|
| 1893 |
+
},
|
| 1894 |
+
"node_modules/tslib": {
|
| 1895 |
+
"version": "2.8.1",
|
| 1896 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 1897 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 1898 |
+
"license": "0BSD"
|
| 1899 |
+
},
|
| 1900 |
+
"node_modules/type-fest": {
|
| 1901 |
+
"version": "5.6.0",
|
| 1902 |
+
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz",
|
| 1903 |
+
"integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==",
|
| 1904 |
+
"license": "(MIT OR CC0-1.0)",
|
| 1905 |
+
"dependencies": {
|
| 1906 |
+
"tagged-tag": "^1.0.0"
|
| 1907 |
+
},
|
| 1908 |
+
"engines": {
|
| 1909 |
+
"node": ">=20"
|
| 1910 |
+
},
|
| 1911 |
+
"funding": {
|
| 1912 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1913 |
+
}
|
| 1914 |
+
},
|
| 1915 |
+
"node_modules/type-is": {
|
| 1916 |
+
"version": "1.6.18",
|
| 1917 |
+
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
| 1918 |
+
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
| 1919 |
+
"license": "MIT",
|
| 1920 |
+
"dependencies": {
|
| 1921 |
+
"media-typer": "0.3.0",
|
| 1922 |
+
"mime-types": "~2.1.24"
|
| 1923 |
+
},
|
| 1924 |
+
"engines": {
|
| 1925 |
+
"node": ">= 0.6"
|
| 1926 |
+
}
|
| 1927 |
+
},
|
| 1928 |
+
"node_modules/typescript": {
|
| 1929 |
+
"version": "5.9.3",
|
| 1930 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 1931 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 1932 |
+
"license": "Apache-2.0",
|
| 1933 |
+
"bin": {
|
| 1934 |
+
"tsc": "bin/tsc",
|
| 1935 |
+
"tsserver": "bin/tsserver"
|
| 1936 |
+
},
|
| 1937 |
+
"engines": {
|
| 1938 |
+
"node": ">=14.17"
|
| 1939 |
+
}
|
| 1940 |
+
},
|
| 1941 |
+
"node_modules/undici-types": {
|
| 1942 |
+
"version": "7.19.2",
|
| 1943 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
| 1944 |
+
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
| 1945 |
+
"license": "MIT"
|
| 1946 |
+
},
|
| 1947 |
+
"node_modules/unpipe": {
|
| 1948 |
+
"version": "1.0.0",
|
| 1949 |
+
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
| 1950 |
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
| 1951 |
+
"license": "MIT",
|
| 1952 |
+
"engines": {
|
| 1953 |
+
"node": ">= 0.8"
|
| 1954 |
+
}
|
| 1955 |
+
},
|
| 1956 |
+
"node_modules/until-async": {
|
| 1957 |
+
"version": "3.0.2",
|
| 1958 |
+
"resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
|
| 1959 |
+
"integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
|
| 1960 |
+
"license": "MIT",
|
| 1961 |
+
"funding": {
|
| 1962 |
+
"url": "https://github.com/sponsors/kettanaito"
|
| 1963 |
+
}
|
| 1964 |
+
},
|
| 1965 |
+
"node_modules/utils-merge": {
|
| 1966 |
+
"version": "1.0.1",
|
| 1967 |
+
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
| 1968 |
+
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
| 1969 |
+
"license": "MIT",
|
| 1970 |
+
"engines": {
|
| 1971 |
+
"node": ">= 0.4.0"
|
| 1972 |
+
}
|
| 1973 |
+
},
|
| 1974 |
+
"node_modules/vary": {
|
| 1975 |
+
"version": "1.1.2",
|
| 1976 |
+
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
| 1977 |
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
| 1978 |
+
"license": "MIT",
|
| 1979 |
+
"engines": {
|
| 1980 |
+
"node": ">= 0.8"
|
| 1981 |
+
}
|
| 1982 |
+
},
|
| 1983 |
+
"node_modules/web-streams-polyfill": {
|
| 1984 |
+
"version": "3.3.3",
|
| 1985 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
| 1986 |
+
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
| 1987 |
+
"license": "MIT",
|
| 1988 |
+
"engines": {
|
| 1989 |
+
"node": ">= 8"
|
| 1990 |
+
}
|
| 1991 |
+
},
|
| 1992 |
+
"node_modules/wrap-ansi": {
|
| 1993 |
+
"version": "7.0.0",
|
| 1994 |
+
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
| 1995 |
+
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
| 1996 |
+
"license": "MIT",
|
| 1997 |
+
"dependencies": {
|
| 1998 |
+
"ansi-styles": "^4.0.0",
|
| 1999 |
+
"string-width": "^4.1.0",
|
| 2000 |
+
"strip-ansi": "^6.0.0"
|
| 2001 |
+
},
|
| 2002 |
+
"engines": {
|
| 2003 |
+
"node": ">=10"
|
| 2004 |
+
},
|
| 2005 |
+
"funding": {
|
| 2006 |
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
| 2007 |
+
}
|
| 2008 |
+
},
|
| 2009 |
+
"node_modules/ws": {
|
| 2010 |
+
"version": "8.20.0",
|
| 2011 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
| 2012 |
+
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
| 2013 |
+
"license": "MIT",
|
| 2014 |
+
"engines": {
|
| 2015 |
+
"node": ">=10.0.0"
|
| 2016 |
+
},
|
| 2017 |
+
"peerDependencies": {
|
| 2018 |
+
"bufferutil": "^4.0.1",
|
| 2019 |
+
"utf-8-validate": ">=5.0.2"
|
| 2020 |
+
},
|
| 2021 |
+
"peerDependenciesMeta": {
|
| 2022 |
+
"bufferutil": {
|
| 2023 |
+
"optional": true
|
| 2024 |
+
},
|
| 2025 |
+
"utf-8-validate": {
|
| 2026 |
+
"optional": true
|
| 2027 |
+
}
|
| 2028 |
+
}
|
| 2029 |
+
},
|
| 2030 |
+
"node_modules/xtend": {
|
| 2031 |
+
"version": "4.0.2",
|
| 2032 |
+
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
| 2033 |
+
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
| 2034 |
+
"license": "MIT",
|
| 2035 |
+
"engines": {
|
| 2036 |
+
"node": ">=0.4"
|
| 2037 |
+
}
|
| 2038 |
+
},
|
| 2039 |
+
"node_modules/y18n": {
|
| 2040 |
+
"version": "5.0.8",
|
| 2041 |
+
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
| 2042 |
+
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
| 2043 |
+
"license": "ISC",
|
| 2044 |
+
"engines": {
|
| 2045 |
+
"node": ">=10"
|
| 2046 |
+
}
|
| 2047 |
+
},
|
| 2048 |
+
"node_modules/yargs": {
|
| 2049 |
+
"version": "17.7.2",
|
| 2050 |
+
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
| 2051 |
+
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
| 2052 |
+
"license": "MIT",
|
| 2053 |
+
"dependencies": {
|
| 2054 |
+
"cliui": "^8.0.1",
|
| 2055 |
+
"escalade": "^3.1.1",
|
| 2056 |
+
"get-caller-file": "^2.0.5",
|
| 2057 |
+
"require-directory": "^2.1.1",
|
| 2058 |
+
"string-width": "^4.2.3",
|
| 2059 |
+
"y18n": "^5.0.5",
|
| 2060 |
+
"yargs-parser": "^21.1.1"
|
| 2061 |
+
},
|
| 2062 |
+
"engines": {
|
| 2063 |
+
"node": ">=12"
|
| 2064 |
+
}
|
| 2065 |
+
},
|
| 2066 |
+
"node_modules/yargs-parser": {
|
| 2067 |
+
"version": "21.1.1",
|
| 2068 |
+
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
| 2069 |
+
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
| 2070 |
+
"license": "ISC",
|
| 2071 |
+
"engines": {
|
| 2072 |
+
"node": ">=12"
|
| 2073 |
+
}
|
| 2074 |
+
}
|
| 2075 |
+
}
|
| 2076 |
+
}
|
package.json
CHANGED
|
@@ -5,17 +5,19 @@
|
|
| 5 |
"main": "server/index.js",
|
| 6 |
"scripts": {
|
| 7 |
"start": "node server/index.js",
|
| 8 |
-
"dev": "node --watch server/index.js"
|
|
|
|
| 9 |
},
|
| 10 |
"dependencies": {
|
| 11 |
"@gradio/client": "^0.20.1",
|
| 12 |
"@supabase/supabase-js": "^2.39.7",
|
| 13 |
"express": "^4.18.2",
|
| 14 |
"html-to-text": "^9.0.5",
|
| 15 |
-
"ws": "^8.16.0",
|
| 16 |
"openai": "^6.29.0",
|
| 17 |
"node-fetch": "^3.3.2",
|
| 18 |
"express-rate-limit": "8.3.2",
|
|
|
|
|
|
|
| 19 |
"tiktoken": "^1.0.0"
|
| 20 |
}
|
| 21 |
}
|
|
|
|
| 5 |
"main": "server/index.js",
|
| 6 |
"scripts": {
|
| 7 |
"start": "node server/index.js",
|
| 8 |
+
"dev": "node --watch server/index.js",
|
| 9 |
+
"migrate:postgres": "node scripts/migrate-to-postgres.js"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
"@gradio/client": "^0.20.1",
|
| 13 |
"@supabase/supabase-js": "^2.39.7",
|
| 14 |
"express": "^4.18.2",
|
| 15 |
"html-to-text": "^9.0.5",
|
|
|
|
| 16 |
"openai": "^6.29.0",
|
| 17 |
"node-fetch": "^3.3.2",
|
| 18 |
"express-rate-limit": "8.3.2",
|
| 19 |
+
"pg": "^8.16.3",
|
| 20 |
+
"ws": "^8.16.0",
|
| 21 |
"tiktoken": "^1.0.0"
|
| 22 |
}
|
| 23 |
}
|
scripts/migrate-to-postgres.js
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs/promises';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { createClient } from '@supabase/supabase-js';
|
| 4 |
+
import { loadEncryptedJson } from '../server/cryptoUtils.js';
|
| 5 |
+
import {
|
| 6 |
+
POSTGRES_STORAGE_DIR,
|
| 7 |
+
POSTGRES_STORAGE_MANIFEST,
|
| 8 |
+
DATA_ROOT,
|
| 9 |
+
} from '../server/dataPaths.js';
|
| 10 |
+
import {
|
| 11 |
+
createStandalonePostgresPool,
|
| 12 |
+
encryptJsonPayload,
|
| 13 |
+
makeLookupToken,
|
| 14 |
+
makeOwnerLookup,
|
| 15 |
+
} from '../server/postgres.js';
|
| 16 |
+
import { POSTGRES_SCHEMA_SQL } from '../server/postgresSchema.js';
|
| 17 |
+
import { SUPABASE_URL } from '../server/config.js';
|
| 18 |
+
|
| 19 |
+
const TEMP_TTL_MS = 24 * 60 * 60 * 1000;
|
| 20 |
+
const TEMP_INACTIVITY = 12 * 60 * 60 * 1000;
|
| 21 |
+
const FEEDBACK_AAD = 'feedback_tickets_v1';
|
| 22 |
+
const VERSION_FILE = path.join(DATA_ROOT, 'version.json');
|
| 23 |
+
const WEB_SEARCH_USAGE_FILE = path.join(DATA_ROOT, 'web-search-usage.json');
|
| 24 |
+
const MEDIA_INDEX_FILE = path.join(DATA_ROOT, 'media', 'index.json');
|
| 25 |
+
const MEDIA_BLOBS_DIR = path.join(DATA_ROOT, 'media', 'blobs');
|
| 26 |
+
const TEMP_STORE_FILE = path.join(DATA_ROOT, 'temp_sessions.json');
|
| 27 |
+
const MEMORIES_FILE = path.join(DATA_ROOT, 'memories', 'index.json');
|
| 28 |
+
const DELETED_CHATS_FILE = path.join(DATA_ROOT, 'deleted_chats', 'index.json');
|
| 29 |
+
const SYSTEM_PROMPTS_FILE = path.join(DATA_ROOT, 'system-prompts', 'index.json');
|
| 30 |
+
const FEEDBACK_FILE = path.join(DATA_ROOT, 'feedback_tickets.json');
|
| 31 |
+
const GUEST_REQUEST_FILE = path.join(DATA_ROOT, 'guest_request_counts.json');
|
| 32 |
+
|
| 33 |
+
function nowIso() {
|
| 34 |
+
return new Date().toISOString();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function sessionAad(scopeType, sessionId) {
|
| 38 |
+
return `chat-session:${scopeType}:${sessionId}`;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function guestStateLookup(tempId) {
|
| 42 |
+
return makeLookupToken('guest-state', tempId);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function guestStateAad(tempId) {
|
| 46 |
+
return `guest-state:${tempId}`;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function guestExpiryRecord(tempData) {
|
| 50 |
+
const createdExpires = (tempData.created || Date.now()) + TEMP_TTL_MS;
|
| 51 |
+
const inactiveExpires = (tempData.lastActive || Date.now()) + TEMP_INACTIVITY;
|
| 52 |
+
return new Date(Math.min(createdExpires, inactiveExpires)).toISOString();
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function shareTokenLookup(token) {
|
| 56 |
+
return makeLookupToken('session-share-token', token);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function shareAad(recordId) {
|
| 60 |
+
return `session-share:${recordId}`;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function promptLookup(userId) {
|
| 64 |
+
return makeLookupToken('system-prompt', userId);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function promptAad(userId) {
|
| 68 |
+
return `system-prompt:${userId}`;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function mediaEntryAad(id) {
|
| 72 |
+
return `media-entry:${id}`;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function feedbackAad(id) {
|
| 76 |
+
return `feedback:${id}`;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function usernameLookup(username) {
|
| 80 |
+
return makeLookupToken('username', username);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function versionLookup(publicUrl) {
|
| 84 |
+
return makeLookupToken('app-version', publicUrl);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function versionAad(publicUrl) {
|
| 88 |
+
return `app-version:${publicUrl}`;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function requestLookup(ip) {
|
| 92 |
+
return makeLookupToken('guest-request', ip);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function webUsageLookup(key) {
|
| 96 |
+
return makeLookupToken('web-search-usage', key);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function webUsageAad(key, day) {
|
| 100 |
+
return `web-search-usage:${key}:${day}`;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
async function fileExists(filePath) {
|
| 104 |
+
try {
|
| 105 |
+
await fs.access(filePath);
|
| 106 |
+
return true;
|
| 107 |
+
} catch {
|
| 108 |
+
return false;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
async function readJsonIfExists(filePath) {
|
| 113 |
+
try {
|
| 114 |
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
| 115 |
+
} catch {
|
| 116 |
+
return null;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
async function fetchAllSupabaseRows(client, tableName) {
|
| 121 |
+
const pageSize = 1000;
|
| 122 |
+
const rows = [];
|
| 123 |
+
let from = 0;
|
| 124 |
+
while (true) {
|
| 125 |
+
const { data, error } = await client
|
| 126 |
+
.from(tableName)
|
| 127 |
+
.select('*')
|
| 128 |
+
.range(from, from + pageSize - 1);
|
| 129 |
+
if (error) throw error;
|
| 130 |
+
rows.push(...(data || []));
|
| 131 |
+
if (!data || data.length < pageSize) break;
|
| 132 |
+
from += pageSize;
|
| 133 |
+
}
|
| 134 |
+
return rows;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
async function truncateTargetTables(pool) {
|
| 138 |
+
await pool.query(`
|
| 139 |
+
TRUNCATE TABLE
|
| 140 |
+
media_blobs,
|
| 141 |
+
media_entries,
|
| 142 |
+
deleted_chats,
|
| 143 |
+
memories,
|
| 144 |
+
system_prompts,
|
| 145 |
+
feedback_tickets,
|
| 146 |
+
guest_request_counters,
|
| 147 |
+
web_search_usage,
|
| 148 |
+
user_settings,
|
| 149 |
+
user_profiles,
|
| 150 |
+
device_sessions,
|
| 151 |
+
session_shares,
|
| 152 |
+
chat_sessions,
|
| 153 |
+
guest_state,
|
| 154 |
+
app_versions
|
| 155 |
+
RESTART IDENTITY
|
| 156 |
+
CASCADE
|
| 157 |
+
`);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
async function migrateVersions(pool, report) {
|
| 161 |
+
const data = await readJsonIfExists(VERSION_FILE);
|
| 162 |
+
const entries = Array.isArray(data) ? data : [];
|
| 163 |
+
let count = 0;
|
| 164 |
+
for (const entry of entries) {
|
| 165 |
+
for (const [publicUrl, sha] of Object.entries(entry || {})) {
|
| 166 |
+
await pool.query(
|
| 167 |
+
`INSERT INTO app_versions (public_url_lookup, updated_at, payload)
|
| 168 |
+
VALUES ($1, $2, $3::jsonb)`,
|
| 169 |
+
[
|
| 170 |
+
versionLookup(publicUrl),
|
| 171 |
+
nowIso(),
|
| 172 |
+
JSON.stringify(encryptJsonPayload({ publicUrl, sha }, versionAad(publicUrl))),
|
| 173 |
+
]
|
| 174 |
+
);
|
| 175 |
+
count += 1;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
report.migrated.versionEntries = count;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
async function migrateTempSessions(pool, report) {
|
| 182 |
+
const data = await loadEncryptedJson(TEMP_STORE_FILE);
|
| 183 |
+
const records = data || {};
|
| 184 |
+
let ownerCount = 0;
|
| 185 |
+
let sessionCount = 0;
|
| 186 |
+
|
| 187 |
+
for (const [tempId, tempData] of Object.entries(records)) {
|
| 188 |
+
ownerCount += 1;
|
| 189 |
+
const owner = { type: 'guest', id: tempId };
|
| 190 |
+
await pool.query(
|
| 191 |
+
`INSERT INTO guest_state (owner_lookup, expires_at, updated_at, payload)
|
| 192 |
+
VALUES ($1, $2, $3, $4::jsonb)`,
|
| 193 |
+
[
|
| 194 |
+
guestStateLookup(tempId),
|
| 195 |
+
guestExpiryRecord(tempData),
|
| 196 |
+
nowIso(),
|
| 197 |
+
JSON.stringify(encryptJsonPayload({
|
| 198 |
+
tempId,
|
| 199 |
+
msgCount: tempData.msgCount || 0,
|
| 200 |
+
created: tempData.created || Date.now(),
|
| 201 |
+
lastActive: tempData.lastActive || Date.now(),
|
| 202 |
+
}, guestStateAad(tempId))),
|
| 203 |
+
]
|
| 204 |
+
);
|
| 205 |
+
|
| 206 |
+
for (const session of Object.values(tempData.sessions || {})) {
|
| 207 |
+
await pool.query(
|
| 208 |
+
`INSERT INTO chat_sessions (id, scope_type, owner_lookup, created_at, updated_at, expires_at, payload)
|
| 209 |
+
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
|
| 210 |
+
[
|
| 211 |
+
session.id,
|
| 212 |
+
'guest',
|
| 213 |
+
makeOwnerLookup(owner),
|
| 214 |
+
new Date(session.created || Date.now()).toISOString(),
|
| 215 |
+
nowIso(),
|
| 216 |
+
guestExpiryRecord(tempData),
|
| 217 |
+
JSON.stringify(encryptJsonPayload(session, sessionAad('guest', session.id))),
|
| 218 |
+
]
|
| 219 |
+
);
|
| 220 |
+
sessionCount += 1;
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
report.migrated.tempOwners = ownerCount;
|
| 225 |
+
report.migrated.tempSessions = sessionCount;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
async function migrateMemories(pool, report) {
|
| 229 |
+
const data = await loadEncryptedJson(MEMORIES_FILE);
|
| 230 |
+
const memories = Object.values(data?.memories || {});
|
| 231 |
+
for (const memory of memories) {
|
| 232 |
+
await pool.query(
|
| 233 |
+
`INSERT INTO memories (id, owner_lookup, created_at, updated_at, payload)
|
| 234 |
+
VALUES ($1, $2, $3, $4, $5::jsonb)`,
|
| 235 |
+
[
|
| 236 |
+
memory.id,
|
| 237 |
+
makeOwnerLookup({ type: memory.ownerType, id: memory.ownerId }),
|
| 238 |
+
memory.createdAt,
|
| 239 |
+
memory.updatedAt,
|
| 240 |
+
JSON.stringify(encryptJsonPayload(memory, `memory:${memory.id}`)),
|
| 241 |
+
]
|
| 242 |
+
);
|
| 243 |
+
}
|
| 244 |
+
report.migrated.memories = memories.length;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
async function migrateDeletedChats(pool, report) {
|
| 248 |
+
const data = await loadEncryptedJson(DELETED_CHATS_FILE);
|
| 249 |
+
const deletedChats = Object.values(data?.deletedChats || {});
|
| 250 |
+
for (const record of deletedChats) {
|
| 251 |
+
await pool.query(
|
| 252 |
+
`INSERT INTO deleted_chats (id, owner_lookup, purge_at, deleted_at, payload)
|
| 253 |
+
VALUES ($1, $2, $3, $4, $5::jsonb)`,
|
| 254 |
+
[
|
| 255 |
+
record.id,
|
| 256 |
+
makeOwnerLookup({ type: record.ownerType, id: record.ownerId }),
|
| 257 |
+
record.purgeAt || null,
|
| 258 |
+
record.deletedAt,
|
| 259 |
+
JSON.stringify(encryptJsonPayload(record, `deleted-chat:${record.id}`)),
|
| 260 |
+
]
|
| 261 |
+
);
|
| 262 |
+
}
|
| 263 |
+
report.migrated.deletedChats = deletedChats.length;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
async function migrateSystemPrompts(pool, report) {
|
| 267 |
+
const data = await loadEncryptedJson(SYSTEM_PROMPTS_FILE, 'system-prompts');
|
| 268 |
+
const prompts = Object.entries(data?.prompts || {});
|
| 269 |
+
for (const [userId, prompt] of prompts) {
|
| 270 |
+
const record = {
|
| 271 |
+
userId,
|
| 272 |
+
markdown: prompt.markdown,
|
| 273 |
+
updatedAt: prompt.updatedAt || nowIso(),
|
| 274 |
+
};
|
| 275 |
+
await pool.query(
|
| 276 |
+
`INSERT INTO system_prompts (owner_lookup, updated_at, payload)
|
| 277 |
+
VALUES ($1, $2, $3::jsonb)`,
|
| 278 |
+
[
|
| 279 |
+
promptLookup(userId),
|
| 280 |
+
record.updatedAt,
|
| 281 |
+
JSON.stringify(encryptJsonPayload(record, promptAad(userId))),
|
| 282 |
+
]
|
| 283 |
+
);
|
| 284 |
+
}
|
| 285 |
+
report.migrated.systemPrompts = prompts.length;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
async function migrateFeedback(pool, report) {
|
| 289 |
+
const data = await loadEncryptedJson(FEEDBACK_FILE, FEEDBACK_AAD);
|
| 290 |
+
const tickets = Array.isArray(data?.tickets) ? data.tickets : [];
|
| 291 |
+
for (const ticket of tickets) {
|
| 292 |
+
await pool.query(
|
| 293 |
+
`INSERT INTO feedback_tickets (id, status, submitted_at, payload)
|
| 294 |
+
VALUES ($1, $2, $3, $4::jsonb)`,
|
| 295 |
+
[
|
| 296 |
+
ticket.id,
|
| 297 |
+
ticket.status || 'open',
|
| 298 |
+
ticket.submittedAt,
|
| 299 |
+
JSON.stringify(encryptJsonPayload(ticket, feedbackAad(ticket.id))),
|
| 300 |
+
]
|
| 301 |
+
);
|
| 302 |
+
}
|
| 303 |
+
report.migrated.feedbackTickets = tickets.length;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
async function migrateGuestRequestCounters(pool, report) {
|
| 307 |
+
const data = await loadEncryptedJson(GUEST_REQUEST_FILE);
|
| 308 |
+
const entries = Object.entries(data || {});
|
| 309 |
+
for (const [ip, entry] of entries) {
|
| 310 |
+
await pool.query(
|
| 311 |
+
`INSERT INTO guest_request_counters (key_lookup, expires_at, updated_at, payload)
|
| 312 |
+
VALUES ($1, $2, $3, $4::jsonb)`,
|
| 313 |
+
[
|
| 314 |
+
requestLookup(ip),
|
| 315 |
+
new Date(entry.resetAt || Date.now()).toISOString(),
|
| 316 |
+
nowIso(),
|
| 317 |
+
JSON.stringify(encryptJsonPayload({
|
| 318 |
+
ip,
|
| 319 |
+
count: entry.count || 0,
|
| 320 |
+
resetAt: entry.resetAt || Date.now(),
|
| 321 |
+
}, 'guest-request-row')),
|
| 322 |
+
]
|
| 323 |
+
);
|
| 324 |
+
}
|
| 325 |
+
report.migrated.guestRequestCounters = entries.length;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
async function migrateWebSearchUsage(pool, report) {
|
| 329 |
+
const data = await readJsonIfExists(WEB_SEARCH_USAGE_FILE);
|
| 330 |
+
const days = data?.days && typeof data.days === 'object' ? data.days : {};
|
| 331 |
+
let count = 0;
|
| 332 |
+
for (const [dayKey, keys] of Object.entries(days)) {
|
| 333 |
+
for (const [key, used] of Object.entries(keys || {})) {
|
| 334 |
+
await pool.query(
|
| 335 |
+
`INSERT INTO web_search_usage (key_lookup, day_key, updated_at, payload)
|
| 336 |
+
VALUES ($1, $2, $3, $4::jsonb)`,
|
| 337 |
+
[
|
| 338 |
+
webUsageLookup(key),
|
| 339 |
+
dayKey,
|
| 340 |
+
nowIso(),
|
| 341 |
+
JSON.stringify(encryptJsonPayload({ used }, webUsageAad(key, dayKey))),
|
| 342 |
+
]
|
| 343 |
+
);
|
| 344 |
+
count += 1;
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
report.migrated.webSearchUsageRows = count;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
async function migrateMedia(pool, report) {
|
| 351 |
+
const data = await loadEncryptedJson(MEDIA_INDEX_FILE);
|
| 352 |
+
const entries = Object.values(data?.entries || {});
|
| 353 |
+
let blobCount = 0;
|
| 354 |
+
for (const entry of entries) {
|
| 355 |
+
await pool.query(
|
| 356 |
+
`INSERT INTO media_entries (
|
| 357 |
+
id, owner_lookup, parent_id, entry_type, updated_at, created_at,
|
| 358 |
+
trashed_at, purge_at, expires_at, size_bytes, payload
|
| 359 |
+
)
|
| 360 |
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb)`,
|
| 361 |
+
[
|
| 362 |
+
entry.id,
|
| 363 |
+
makeOwnerLookup({ type: entry.ownerType, id: entry.ownerId }),
|
| 364 |
+
entry.parentId || null,
|
| 365 |
+
entry.type,
|
| 366 |
+
entry.updatedAt || entry.createdAt,
|
| 367 |
+
entry.createdAt,
|
| 368 |
+
entry.trashedAt || null,
|
| 369 |
+
entry.purgeAt || null,
|
| 370 |
+
entry.expiresAt || null,
|
| 371 |
+
entry.size || 0,
|
| 372 |
+
JSON.stringify(encryptJsonPayload(entry, mediaEntryAad(entry.id))),
|
| 373 |
+
]
|
| 374 |
+
);
|
| 375 |
+
|
| 376 |
+
if (entry.type === 'file') {
|
| 377 |
+
const blobPath = path.join(MEDIA_BLOBS_DIR, `${entry.id}.bin`);
|
| 378 |
+
if (await fileExists(blobPath)) {
|
| 379 |
+
const blob = await fs.readFile(blobPath);
|
| 380 |
+
await pool.query(
|
| 381 |
+
`INSERT INTO media_blobs (entry_id, updated_at, payload)
|
| 382 |
+
VALUES ($1, $2, $3)`,
|
| 383 |
+
[entry.id, entry.updatedAt || entry.createdAt, blob]
|
| 384 |
+
);
|
| 385 |
+
blobCount += 1;
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
report.migrated.mediaEntries = entries.length;
|
| 390 |
+
report.migrated.mediaBlobs = blobCount;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
async function migrateSupabaseData(pool, report) {
|
| 394 |
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
|
| 395 |
+
if (!serviceRoleKey) {
|
| 396 |
+
if (process.env.ALLOW_PARTIAL_SQL_MIGRATION === '1') {
|
| 397 |
+
report.pending.push(
|
| 398 |
+
'Supabase-backed user tables were not exported because SUPABASE_SERVICE_ROLE_KEY is not set.'
|
| 399 |
+
);
|
| 400 |
+
return;
|
| 401 |
+
}
|
| 402 |
+
throw new Error(
|
| 403 |
+
'SUPABASE_SERVICE_ROLE_KEY is required to migrate existing Supabase-backed sessions, shares, settings, and profiles before enabling PostgreSQL-only mode. Set ALLOW_PARTIAL_SQL_MIGRATION=1 only if you intentionally want to skip those records.'
|
| 404 |
+
);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
const supabase = createClient(SUPABASE_URL, serviceRoleKey, {
|
| 408 |
+
auth: { persistSession: false },
|
| 409 |
+
});
|
| 410 |
+
|
| 411 |
+
const [webSessions, sharedSessions, userSettings, profiles] = await Promise.all([
|
| 412 |
+
fetchAllSupabaseRows(supabase, 'web_sessions'),
|
| 413 |
+
fetchAllSupabaseRows(supabase, 'shared_sessions'),
|
| 414 |
+
fetchAllSupabaseRows(supabase, 'user_settings'),
|
| 415 |
+
fetchAllSupabaseRows(supabase, 'profiles'),
|
| 416 |
+
]);
|
| 417 |
+
|
| 418 |
+
for (const row of webSessions) {
|
| 419 |
+
const session = {
|
| 420 |
+
id: row.id,
|
| 421 |
+
name: row.name,
|
| 422 |
+
created: new Date(row.created_at).getTime(),
|
| 423 |
+
history: row.history || [],
|
| 424 |
+
model: row.model || null,
|
| 425 |
+
};
|
| 426 |
+
await pool.query(
|
| 427 |
+
`INSERT INTO chat_sessions (id, scope_type, owner_lookup, created_at, updated_at, expires_at, payload)
|
| 428 |
+
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
|
| 429 |
+
[
|
| 430 |
+
row.id,
|
| 431 |
+
'user',
|
| 432 |
+
makeOwnerLookup({ type: 'user', id: row.user_id }),
|
| 433 |
+
row.created_at,
|
| 434 |
+
row.updated_at || nowIso(),
|
| 435 |
+
null,
|
| 436 |
+
JSON.stringify(encryptJsonPayload(session, sessionAad('user', row.id))),
|
| 437 |
+
]
|
| 438 |
+
);
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
for (const row of sharedSessions) {
|
| 442 |
+
const lookup = shareTokenLookup(row.token);
|
| 443 |
+
const id = row.id || `share_${lookup.slice(0, 24)}`;
|
| 444 |
+
await pool.query(
|
| 445 |
+
`INSERT INTO session_shares (id, token_lookup, owner_lookup, created_at, payload)
|
| 446 |
+
VALUES ($1, $2, $3, $4, $5::jsonb)`,
|
| 447 |
+
[
|
| 448 |
+
id,
|
| 449 |
+
lookup,
|
| 450 |
+
makeOwnerLookup({ type: 'user', id: row.owner_id }),
|
| 451 |
+
row.created_at || nowIso(),
|
| 452 |
+
JSON.stringify(encryptJsonPayload({
|
| 453 |
+
id,
|
| 454 |
+
ownerId: row.owner_id,
|
| 455 |
+
sessionSnapshot: row.session_snapshot,
|
| 456 |
+
createdAt: row.created_at || nowIso(),
|
| 457 |
+
}, shareAad(id))),
|
| 458 |
+
]
|
| 459 |
+
);
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
for (const row of userSettings) {
|
| 463 |
+
await pool.query(
|
| 464 |
+
`INSERT INTO user_settings (owner_lookup, updated_at, payload)
|
| 465 |
+
VALUES ($1, $2, $3::jsonb)`,
|
| 466 |
+
[
|
| 467 |
+
makeOwnerLookup({ type: 'user', id: row.user_id }),
|
| 468 |
+
row.updated_at || nowIso(),
|
| 469 |
+
JSON.stringify(encryptJsonPayload({
|
| 470 |
+
userId: row.user_id,
|
| 471 |
+
settings: row.settings || {},
|
| 472 |
+
updatedAt: row.updated_at || nowIso(),
|
| 473 |
+
}, `user-settings:${row.user_id}`)),
|
| 474 |
+
]
|
| 475 |
+
);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
for (const row of profiles) {
|
| 479 |
+
if (!row.id || !row.username) continue;
|
| 480 |
+
await pool.query(
|
| 481 |
+
`INSERT INTO user_profiles (owner_lookup, username_lookup, updated_at, payload)
|
| 482 |
+
VALUES ($1, $2, $3, $4::jsonb)`,
|
| 483 |
+
[
|
| 484 |
+
makeOwnerLookup({ type: 'user', id: row.id }),
|
| 485 |
+
usernameLookup(row.username),
|
| 486 |
+
row.updated_at || nowIso(),
|
| 487 |
+
JSON.stringify(encryptJsonPayload({
|
| 488 |
+
userId: row.id,
|
| 489 |
+
username: row.username,
|
| 490 |
+
updatedAt: row.updated_at || nowIso(),
|
| 491 |
+
}, `user-profile:${row.id}`)),
|
| 492 |
+
]
|
| 493 |
+
);
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
report.migrated.supabaseWebSessions = webSessions.length;
|
| 497 |
+
report.migrated.supabaseSharedSessions = sharedSessions.length;
|
| 498 |
+
report.migrated.supabaseUserSettings = userSettings.length;
|
| 499 |
+
report.migrated.supabaseProfiles = profiles.length;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
async function writeStorageFolder(report) {
|
| 503 |
+
const tempDir = `${POSTGRES_STORAGE_DIR}.tmp`;
|
| 504 |
+
await fs.rm(tempDir, { recursive: true, force: true });
|
| 505 |
+
await fs.mkdir(tempDir, { recursive: true });
|
| 506 |
+
await fs.writeFile(
|
| 507 |
+
path.join(tempDir, path.basename(POSTGRES_STORAGE_MANIFEST)),
|
| 508 |
+
JSON.stringify({
|
| 509 |
+
createdAt: report.completedAt,
|
| 510 |
+
storageMode: 'postgres',
|
| 511 |
+
status: report.status,
|
| 512 |
+
pending: report.pending,
|
| 513 |
+
}, null, 2),
|
| 514 |
+
'utf8'
|
| 515 |
+
);
|
| 516 |
+
await fs.writeFile(path.join(tempDir, 'schema.sql'), POSTGRES_SCHEMA_SQL, 'utf8');
|
| 517 |
+
await fs.writeFile(path.join(tempDir, 'migration-report.json'), JSON.stringify(report, null, 2), 'utf8');
|
| 518 |
+
await fs.writeFile(
|
| 519 |
+
path.join(tempDir, 'README.txt'),
|
| 520 |
+
[
|
| 521 |
+
'This folder marks PostgreSQL storage as active for the backend.',
|
| 522 |
+
'The server checks for this folder on startup and uses PostgreSQL-only mode when it exists.',
|
| 523 |
+
'schema.sql contains the schema used for the migrated encrypted SQL backend.',
|
| 524 |
+
].join('\n'),
|
| 525 |
+
'utf8'
|
| 526 |
+
);
|
| 527 |
+
await fs.rename(tempDir, POSTGRES_STORAGE_DIR);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
async function main() {
|
| 531 |
+
if (await fileExists(POSTGRES_STORAGE_DIR)) {
|
| 532 |
+
throw new Error(`SQL storage folder already exists at ${POSTGRES_STORAGE_DIR}. Remove it before running the migration again.`);
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
await fs.mkdir(DATA_ROOT, { recursive: true });
|
| 536 |
+
|
| 537 |
+
const pool = await createStandalonePostgresPool();
|
| 538 |
+
const report = {
|
| 539 |
+
startedAt: nowIso(),
|
| 540 |
+
targetFolder: POSTGRES_STORAGE_DIR,
|
| 541 |
+
migrated: {},
|
| 542 |
+
pending: [],
|
| 543 |
+
};
|
| 544 |
+
|
| 545 |
+
try {
|
| 546 |
+
await truncateTargetTables(pool);
|
| 547 |
+
await migrateVersions(pool, report);
|
| 548 |
+
await migrateTempSessions(pool, report);
|
| 549 |
+
await migrateMemories(pool, report);
|
| 550 |
+
await migrateDeletedChats(pool, report);
|
| 551 |
+
await migrateSystemPrompts(pool, report);
|
| 552 |
+
await migrateFeedback(pool, report);
|
| 553 |
+
await migrateGuestRequestCounters(pool, report);
|
| 554 |
+
await migrateWebSearchUsage(pool, report);
|
| 555 |
+
await migrateMedia(pool, report);
|
| 556 |
+
await migrateSupabaseData(pool, report);
|
| 557 |
+
|
| 558 |
+
report.completedAt = nowIso();
|
| 559 |
+
report.status = report.pending.length ? 'completed_with_pending_items' : 'completed';
|
| 560 |
+
|
| 561 |
+
await writeStorageFolder(report);
|
| 562 |
+
|
| 563 |
+
console.log('PostgreSQL migration complete.');
|
| 564 |
+
console.log(JSON.stringify(report, null, 2));
|
| 565 |
+
} finally {
|
| 566 |
+
await pool.end();
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
main().catch((err) => {
|
| 571 |
+
console.error('PostgreSQL migration failed:', err);
|
| 572 |
+
process.exitCode = 1;
|
| 573 |
+
});
|
server/auth.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
| 1 |
import { createClient } from '@supabase/supabase-js';
|
| 2 |
import { SUPABASE_URL, SUPABASE_ANON_KEY } from './config.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
| 5 |
|
|
@@ -20,6 +28,16 @@ export async function verifySupabaseToken(accessToken) {
|
|
| 20 |
}
|
| 21 |
|
| 22 |
export async function getUserSettings(userId, accessToken) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
try {
|
| 24 |
const uc = userClient(accessToken);
|
| 25 |
const { data } = await uc.from('user_settings').select('settings')
|
|
@@ -29,6 +47,30 @@ export async function getUserSettings(userId, accessToken) {
|
|
| 29 |
}
|
| 30 |
|
| 31 |
export async function saveUserSettings(userId, accessToken, settings) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
try {
|
| 33 |
const uc = userClient(accessToken);
|
| 34 |
await uc.from('user_settings').upsert({
|
|
@@ -38,6 +80,18 @@ export async function saveUserSettings(userId, accessToken, settings) {
|
|
| 38 |
}
|
| 39 |
|
| 40 |
export async function getUserProfile(userId, accessToken) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
try {
|
| 42 |
const uc = userClient(accessToken);
|
| 43 |
const { data } = await uc.from('profiles').select('username')
|
|
@@ -50,6 +104,37 @@ export async function setUsername(userId, accessToken, username) {
|
|
| 50 |
if (!username?.trim()) return { error: 'Invalid username' };
|
| 51 |
const trimmed = username.trim().toLowerCase().replace(/[^a-z0-9_]/g, '');
|
| 52 |
if (trimmed.length < 3) return { error: 'Username must be at least 3 characters' };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
try {
|
| 54 |
const uc = userClient(accessToken);
|
| 55 |
const { data: existing } = await uc.from('profiles')
|
|
|
|
| 1 |
import { createClient } from '@supabase/supabase-js';
|
| 2 |
import { SUPABASE_URL, SUPABASE_ANON_KEY } from './config.js';
|
| 3 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 4 |
+
import {
|
| 5 |
+
decryptJsonPayload,
|
| 6 |
+
encryptJsonPayload,
|
| 7 |
+
makeLookupToken,
|
| 8 |
+
makeOwnerLookup,
|
| 9 |
+
pgQuery,
|
| 10 |
+
} from './postgres.js';
|
| 11 |
|
| 12 |
const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
| 13 |
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
export async function getUserSettings(userId, accessToken) {
|
| 31 |
+
if (isPostgresStorageMode()) {
|
| 32 |
+
const { rows } = await pgQuery(
|
| 33 |
+
'SELECT payload FROM user_settings WHERE owner_lookup = $1',
|
| 34 |
+
[makeOwnerLookup({ type: 'user', id: userId })]
|
| 35 |
+
);
|
| 36 |
+
const payload = rows[0]
|
| 37 |
+
? decryptJsonPayload(rows[0].payload, `user-settings:${userId}`)
|
| 38 |
+
: null;
|
| 39 |
+
return { ...defaultSettings(), ...(payload?.settings || {}) };
|
| 40 |
+
}
|
| 41 |
try {
|
| 42 |
const uc = userClient(accessToken);
|
| 43 |
const { data } = await uc.from('user_settings').select('settings')
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
export async function saveUserSettings(userId, accessToken, settings) {
|
| 50 |
+
if (isPostgresStorageMode()) {
|
| 51 |
+
try {
|
| 52 |
+
const payload = {
|
| 53 |
+
userId,
|
| 54 |
+
settings,
|
| 55 |
+
updatedAt: new Date().toISOString(),
|
| 56 |
+
};
|
| 57 |
+
await pgQuery(
|
| 58 |
+
`INSERT INTO user_settings (owner_lookup, updated_at, payload)
|
| 59 |
+
VALUES ($1, $2, $3::jsonb)
|
| 60 |
+
ON CONFLICT (owner_lookup)
|
| 61 |
+
DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 62 |
+
[
|
| 63 |
+
makeOwnerLookup({ type: 'user', id: userId }),
|
| 64 |
+
payload.updatedAt,
|
| 65 |
+
JSON.stringify(encryptJsonPayload(payload, `user-settings:${userId}`)),
|
| 66 |
+
]
|
| 67 |
+
);
|
| 68 |
+
return;
|
| 69 |
+
} catch (e) {
|
| 70 |
+
console.error('saveUserSettings', e.message);
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
try {
|
| 75 |
const uc = userClient(accessToken);
|
| 76 |
await uc.from('user_settings').upsert({
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
export async function getUserProfile(userId, accessToken) {
|
| 83 |
+
if (isPostgresStorageMode()) {
|
| 84 |
+
try {
|
| 85 |
+
const { rows } = await pgQuery(
|
| 86 |
+
'SELECT payload FROM user_profiles WHERE owner_lookup = $1',
|
| 87 |
+
[makeOwnerLookup({ type: 'user', id: userId })]
|
| 88 |
+
);
|
| 89 |
+
const payload = rows[0]
|
| 90 |
+
? decryptJsonPayload(rows[0].payload, `user-profile:${userId}`)
|
| 91 |
+
: null;
|
| 92 |
+
return payload?.username ? { username: payload.username } : null;
|
| 93 |
+
} catch { return null; }
|
| 94 |
+
}
|
| 95 |
try {
|
| 96 |
const uc = userClient(accessToken);
|
| 97 |
const { data } = await uc.from('profiles').select('username')
|
|
|
|
| 104 |
if (!username?.trim()) return { error: 'Invalid username' };
|
| 105 |
const trimmed = username.trim().toLowerCase().replace(/[^a-z0-9_]/g, '');
|
| 106 |
if (trimmed.length < 3) return { error: 'Username must be at least 3 characters' };
|
| 107 |
+
if (isPostgresStorageMode()) {
|
| 108 |
+
try {
|
| 109 |
+
const usernameLookup = makeLookupToken('username', trimmed);
|
| 110 |
+
const ownerLookup = makeOwnerLookup({ type: 'user', id: userId });
|
| 111 |
+
const { rows: existingRows } = await pgQuery(
|
| 112 |
+
'SELECT owner_lookup FROM user_profiles WHERE username_lookup = $1',
|
| 113 |
+
[usernameLookup]
|
| 114 |
+
);
|
| 115 |
+
if (existingRows[0] && existingRows[0].owner_lookup !== ownerLookup) {
|
| 116 |
+
return { error: 'Username already taken' };
|
| 117 |
+
}
|
| 118 |
+
const payload = {
|
| 119 |
+
userId,
|
| 120 |
+
username: trimmed,
|
| 121 |
+
updatedAt: new Date().toISOString(),
|
| 122 |
+
};
|
| 123 |
+
await pgQuery(
|
| 124 |
+
`INSERT INTO user_profiles (owner_lookup, username_lookup, updated_at, payload)
|
| 125 |
+
VALUES ($1, $2, $3, $4::jsonb)
|
| 126 |
+
ON CONFLICT (owner_lookup)
|
| 127 |
+
DO UPDATE SET username_lookup = EXCLUDED.username_lookup, updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 128 |
+
[
|
| 129 |
+
ownerLookup,
|
| 130 |
+
usernameLookup,
|
| 131 |
+
payload.updatedAt,
|
| 132 |
+
JSON.stringify(encryptJsonPayload(payload, `user-profile:${userId}`)),
|
| 133 |
+
]
|
| 134 |
+
);
|
| 135 |
+
return { success: true, username: trimmed };
|
| 136 |
+
} catch (e) { return { error: e.message }; }
|
| 137 |
+
}
|
| 138 |
try {
|
| 139 |
const uc = userClient(accessToken);
|
| 140 |
const { data: existing } = await uc.from('profiles')
|
server/chatTrashStore.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
| 1 |
import crypto from 'crypto';
|
| 2 |
import path from 'path';
|
| 3 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const DATA_ROOT = '/data/deleted_chats';
|
| 6 |
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
|
|
@@ -22,8 +29,12 @@ function ensureOwner(owner) {
|
|
| 22 |
return owner;
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
async function ensureLoaded() {
|
| 26 |
-
if (state.loaded) return;
|
| 27 |
const stored = await loadEncryptedJson(INDEX_FILE);
|
| 28 |
state.index = {
|
| 29 |
deletedChats: stored?.deletedChats || {},
|
|
@@ -51,14 +62,59 @@ function sanitize(record) {
|
|
| 51 |
};
|
| 52 |
}
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
const thisStore = {
|
| 55 |
async add(owner, sessionSnapshot) {
|
| 56 |
ensureOwner(owner);
|
| 57 |
-
await ensureLoaded();
|
| 58 |
-
|
| 59 |
const sessionId = sessionSnapshot?.id;
|
| 60 |
if (!sessionId) return null;
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
for (const record of Object.values(state.index.deletedChats)) {
|
| 63 |
if (
|
| 64 |
record.originalSessionId === sessionId &&
|
|
@@ -93,6 +149,12 @@ const thisStore = {
|
|
| 93 |
|
| 94 |
async list(owner) {
|
| 95 |
ensureOwner(owner);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
await ensureLoaded();
|
| 97 |
return Object.values(state.index.deletedChats)
|
| 98 |
.filter((record) => matchesOwner(record, owner))
|
|
@@ -102,6 +164,21 @@ const thisStore = {
|
|
| 102 |
|
| 103 |
async restore(owner, ids) {
|
| 104 |
ensureOwner(owner);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
await ensureLoaded();
|
| 106 |
const restored = [];
|
| 107 |
for (const id of ids || []) {
|
|
@@ -116,6 +193,18 @@ const thisStore = {
|
|
| 116 |
|
| 117 |
async deleteForever(owner, ids) {
|
| 118 |
ensureOwner(owner);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
await ensureLoaded();
|
| 120 |
const removed = [];
|
| 121 |
for (const id of ids || []) {
|
|
@@ -129,6 +218,11 @@ const thisStore = {
|
|
| 129 |
},
|
| 130 |
|
| 131 |
async purgeExpired() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
if (!state.loaded) return;
|
| 133 |
const now = Date.now();
|
| 134 |
let changed = false;
|
|
|
|
| 1 |
import crypto from 'crypto';
|
| 2 |
import path from 'path';
|
| 3 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
| 4 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 5 |
+
import {
|
| 6 |
+
decryptJsonPayload,
|
| 7 |
+
encryptJsonPayload,
|
| 8 |
+
makeOwnerLookup,
|
| 9 |
+
pgQuery,
|
| 10 |
+
} from './postgres.js';
|
| 11 |
|
| 12 |
const DATA_ROOT = '/data/deleted_chats';
|
| 13 |
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
|
|
|
|
| 29 |
return owner;
|
| 30 |
}
|
| 31 |
|
| 32 |
+
function deletedChatAad(id) {
|
| 33 |
+
return `deleted-chat:${id}`;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
async function ensureLoaded() {
|
| 37 |
+
if (state.loaded || isPostgresStorageMode()) return;
|
| 38 |
const stored = await loadEncryptedJson(INDEX_FILE);
|
| 39 |
state.index = {
|
| 40 |
deletedChats: stored?.deletedChats || {},
|
|
|
|
| 62 |
};
|
| 63 |
}
|
| 64 |
|
| 65 |
+
async function listSqlRecords(owner) {
|
| 66 |
+
const { rows } = await pgQuery(
|
| 67 |
+
'SELECT id, payload FROM deleted_chats WHERE owner_lookup = $1 ORDER BY deleted_at DESC',
|
| 68 |
+
[makeOwnerLookup(owner)]
|
| 69 |
+
);
|
| 70 |
+
return rows
|
| 71 |
+
.map((row) => decryptJsonPayload(row.payload, deletedChatAad(row.id)))
|
| 72 |
+
.filter(Boolean);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
async function upsertSqlRecord(record) {
|
| 76 |
+
await pgQuery(
|
| 77 |
+
`INSERT INTO deleted_chats (id, owner_lookup, purge_at, deleted_at, payload)
|
| 78 |
+
VALUES ($1, $2, $3, $4, $5::jsonb)
|
| 79 |
+
ON CONFLICT (id)
|
| 80 |
+
DO UPDATE SET purge_at = EXCLUDED.purge_at, deleted_at = EXCLUDED.deleted_at, payload = EXCLUDED.payload`,
|
| 81 |
+
[
|
| 82 |
+
record.id,
|
| 83 |
+
makeOwnerLookup({ type: record.ownerType, id: record.ownerId }),
|
| 84 |
+
record.purgeAt,
|
| 85 |
+
record.deletedAt,
|
| 86 |
+
JSON.stringify(encryptJsonPayload(record, deletedChatAad(record.id))),
|
| 87 |
+
]
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
const thisStore = {
|
| 92 |
async add(owner, sessionSnapshot) {
|
| 93 |
ensureOwner(owner);
|
|
|
|
|
|
|
| 94 |
const sessionId = sessionSnapshot?.id;
|
| 95 |
if (!sessionId) return null;
|
| 96 |
|
| 97 |
+
if (isPostgresStorageMode()) {
|
| 98 |
+
const existing = (await listSqlRecords(owner)).find((record) => record.originalSessionId === sessionId);
|
| 99 |
+
const deleted = existing || {
|
| 100 |
+
id: crypto.randomUUID(),
|
| 101 |
+
originalSessionId: sessionId,
|
| 102 |
+
ownerType: owner.type,
|
| 103 |
+
ownerId: owner.id,
|
| 104 |
+
sessionSnapshot,
|
| 105 |
+
created: sessionSnapshot.created || Date.now(),
|
| 106 |
+
};
|
| 107 |
+
deleted.sessionSnapshot = sessionSnapshot;
|
| 108 |
+
deleted.name = sessionSnapshot.name || 'Deleted Chat';
|
| 109 |
+
deleted.created = sessionSnapshot.created || deleted.created || Date.now();
|
| 110 |
+
deleted.deletedAt = nowIso();
|
| 111 |
+
deleted.purgeAt = new Date(Date.now() + RETENTION_MS).toISOString();
|
| 112 |
+
await upsertSqlRecord(deleted);
|
| 113 |
+
return sanitize(deleted);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
await ensureLoaded();
|
| 117 |
+
|
| 118 |
for (const record of Object.values(state.index.deletedChats)) {
|
| 119 |
if (
|
| 120 |
record.originalSessionId === sessionId &&
|
|
|
|
| 149 |
|
| 150 |
async list(owner) {
|
| 151 |
ensureOwner(owner);
|
| 152 |
+
if (isPostgresStorageMode()) {
|
| 153 |
+
return (await listSqlRecords(owner))
|
| 154 |
+
.sort((a, b) => new Date(b.deletedAt).getTime() - new Date(a.deletedAt).getTime())
|
| 155 |
+
.map(sanitize);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
await ensureLoaded();
|
| 159 |
return Object.values(state.index.deletedChats)
|
| 160 |
.filter((record) => matchesOwner(record, owner))
|
|
|
|
| 164 |
|
| 165 |
async restore(owner, ids) {
|
| 166 |
ensureOwner(owner);
|
| 167 |
+
if (isPostgresStorageMode()) {
|
| 168 |
+
const restored = [];
|
| 169 |
+
for (const id of ids || []) {
|
| 170 |
+
const { rows } = await pgQuery(
|
| 171 |
+
'SELECT payload FROM deleted_chats WHERE id = $1 AND owner_lookup = $2',
|
| 172 |
+
[id, makeOwnerLookup(owner)]
|
| 173 |
+
);
|
| 174 |
+
const record = rows[0] ? decryptJsonPayload(rows[0].payload, deletedChatAad(id)) : null;
|
| 175 |
+
if (!record || !matchesOwner(record, owner)) continue;
|
| 176 |
+
restored.push(record.sessionSnapshot);
|
| 177 |
+
await pgQuery('DELETE FROM deleted_chats WHERE id = $1', [id]);
|
| 178 |
+
}
|
| 179 |
+
return restored;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
await ensureLoaded();
|
| 183 |
const restored = [];
|
| 184 |
for (const id of ids || []) {
|
|
|
|
| 193 |
|
| 194 |
async deleteForever(owner, ids) {
|
| 195 |
ensureOwner(owner);
|
| 196 |
+
if (isPostgresStorageMode()) {
|
| 197 |
+
const removed = [];
|
| 198 |
+
for (const id of ids || []) {
|
| 199 |
+
const result = await pgQuery(
|
| 200 |
+
'DELETE FROM deleted_chats WHERE id = $1 AND owner_lookup = $2',
|
| 201 |
+
[id, makeOwnerLookup(owner)]
|
| 202 |
+
);
|
| 203 |
+
if (result.rowCount > 0) removed.push(id);
|
| 204 |
+
}
|
| 205 |
+
return removed;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
await ensureLoaded();
|
| 209 |
const removed = [];
|
| 210 |
for (const id of ids || []) {
|
|
|
|
| 218 |
},
|
| 219 |
|
| 220 |
async purgeExpired() {
|
| 221 |
+
if (isPostgresStorageMode()) {
|
| 222 |
+
await pgQuery('DELETE FROM deleted_chats WHERE purge_at IS NOT NULL AND purge_at <= $1', [new Date().toISOString()]);
|
| 223 |
+
return;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
if (!state.loaded) return;
|
| 227 |
const now = Date.now();
|
| 228 |
let changed = false;
|
server/cryptoUtils.js
CHANGED
|
@@ -15,6 +15,13 @@ function getKey() {
|
|
| 15 |
return crypto.createHash('sha256').update(keyEnv).digest().subarray(0, KEY_LENGTH);
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
function normalizeAad(aad = '') {
|
| 19 |
if (Buffer.isBuffer(aad)) return aad;
|
| 20 |
return Buffer.from(String(aad || ''), 'utf8');
|
|
|
|
| 15 |
return crypto.createHash('sha256').update(keyEnv).digest().subarray(0, KEY_LENGTH);
|
| 16 |
}
|
| 17 |
|
| 18 |
+
export function createLookupHash(value, namespace = 'lookup') {
|
| 19 |
+
return crypto
|
| 20 |
+
.createHmac('sha256', getKey())
|
| 21 |
+
.update(`${namespace}:${String(value ?? '')}`)
|
| 22 |
+
.digest('hex');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
function normalizeAad(aad = '') {
|
| 26 |
if (Buffer.isBuffer(aad)) return aad;
|
| 27 |
return Buffer.from(String(aad || ''), 'utf8');
|
server/dataPaths.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
|
| 4 |
+
export const DATA_ROOT = process.env.DATA_ROOT || '/data';
|
| 5 |
+
export const POSTGRES_STORAGE_DIR_NAME = 'postgresql-store';
|
| 6 |
+
export const POSTGRES_STORAGE_DIR = path.join(DATA_ROOT, POSTGRES_STORAGE_DIR_NAME);
|
| 7 |
+
export const POSTGRES_STORAGE_MANIFEST = path.join(POSTGRES_STORAGE_DIR, 'manifest.json');
|
| 8 |
+
|
| 9 |
+
export const STORAGE_MODE = fs.existsSync(POSTGRES_STORAGE_DIR) ? 'postgres' : 'legacy';
|
| 10 |
+
|
| 11 |
+
export function isPostgresStorageMode() {
|
| 12 |
+
return STORAGE_MODE === 'postgres';
|
| 13 |
+
}
|
server/guestRequestLimiter.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
| 1 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
const REQUESTS_FILE = '/data/guest_request_counts.json';
|
| 4 |
const WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
@@ -7,8 +15,16 @@ const MAX_LOGGED_OUT_REQUESTS = 50;
|
|
| 7 |
const ipCounters = new Map();
|
| 8 |
let loaded = false;
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
async function loadGuestCounters() {
|
| 11 |
-
if (loaded) return;
|
| 12 |
loaded = true;
|
| 13 |
const data = await loadEncryptedJson(REQUESTS_FILE);
|
| 14 |
if (!data) return;
|
|
@@ -20,6 +36,24 @@ async function loadGuestCounters() {
|
|
| 20 |
}
|
| 21 |
}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
async function saveGuestCounters() {
|
| 24 |
const data = {};
|
| 25 |
for (const [ip, entry] of ipCounters) {
|
|
@@ -37,8 +71,23 @@ function cleanupExpired() {
|
|
| 37 |
}
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
export async function initGuestRequestLimiter() {
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
cleanupExpired();
|
| 43 |
setInterval(() => {
|
| 44 |
cleanupExpired();
|
|
@@ -46,6 +95,48 @@ export async function initGuestRequestLimiter() {
|
|
| 46 |
}
|
| 47 |
|
| 48 |
export async function consumeGuestRequest(ip) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
await loadGuestCounters();
|
| 50 |
const now = Date.now();
|
| 51 |
let entry = ipCounters.get(ip);
|
|
@@ -57,7 +148,7 @@ export async function consumeGuestRequest(ip) {
|
|
| 57 |
return false;
|
| 58 |
}
|
| 59 |
entry.count += 1;
|
| 60 |
-
await saveGuestCounters().catch(err => console.error('Failed to save guest request counters:', err));
|
| 61 |
return true;
|
| 62 |
}
|
| 63 |
|
|
|
|
| 1 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
| 2 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 3 |
+
import {
|
| 4 |
+
decryptJsonPayload,
|
| 5 |
+
encryptJsonPayload,
|
| 6 |
+
makeLookupToken,
|
| 7 |
+
pgQuery,
|
| 8 |
+
withPgTransaction,
|
| 9 |
+
} from './postgres.js';
|
| 10 |
|
| 11 |
const REQUESTS_FILE = '/data/guest_request_counts.json';
|
| 12 |
const WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
|
|
| 15 |
const ipCounters = new Map();
|
| 16 |
let loaded = false;
|
| 17 |
|
| 18 |
+
function requestLookup(ip) {
|
| 19 |
+
return makeLookupToken('guest-request', ip);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function requestAad(ip) {
|
| 23 |
+
return `guest-request:${requestLookup(ip)}`;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
async function loadGuestCounters() {
|
| 27 |
+
if (loaded || isPostgresStorageMode()) return;
|
| 28 |
loaded = true;
|
| 29 |
const data = await loadEncryptedJson(REQUESTS_FILE);
|
| 30 |
if (!data) return;
|
|
|
|
| 36 |
}
|
| 37 |
}
|
| 38 |
|
| 39 |
+
async function loadSqlGuestCounters() {
|
| 40 |
+
if (loaded || !isPostgresStorageMode()) return;
|
| 41 |
+
loaded = true;
|
| 42 |
+
const now = new Date().toISOString();
|
| 43 |
+
const { rows } = await pgQuery(
|
| 44 |
+
'SELECT payload FROM guest_request_counters WHERE expires_at > $1',
|
| 45 |
+
[now]
|
| 46 |
+
);
|
| 47 |
+
for (const row of rows) {
|
| 48 |
+
const payload = decryptJsonPayload(row.payload, `guest-request-row`);
|
| 49 |
+
if (!payload?.ip) continue;
|
| 50 |
+
ipCounters.set(payload.ip, {
|
| 51 |
+
count: typeof payload.count === 'number' ? payload.count : 0,
|
| 52 |
+
resetAt: typeof payload.resetAt === 'number' ? payload.resetAt : Date.now() + WINDOW_MS,
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
async function saveGuestCounters() {
|
| 58 |
const data = {};
|
| 59 |
for (const [ip, entry] of ipCounters) {
|
|
|
|
| 71 |
}
|
| 72 |
}
|
| 73 |
|
| 74 |
+
async function pruneSqlGuestCounters() {
|
| 75 |
+
await pgQuery('DELETE FROM guest_request_counters WHERE expires_at <= $1', [new Date().toISOString()]);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
export async function initGuestRequestLimiter() {
|
| 79 |
+
if (isPostgresStorageMode()) {
|
| 80 |
+
await loadSqlGuestCounters().catch((err) => console.error('Failed to load SQL guest request counters:', err));
|
| 81 |
+
await pruneSqlGuestCounters().catch((err) => console.error('Failed to prune SQL guest request counters:', err));
|
| 82 |
+
cleanupExpired();
|
| 83 |
+
setInterval(() => {
|
| 84 |
+
cleanupExpired();
|
| 85 |
+
pruneSqlGuestCounters().catch((err) => console.error('Failed to prune SQL guest request counters:', err));
|
| 86 |
+
}, 60 * 60 * 1000);
|
| 87 |
+
return;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
await loadGuestCounters().catch((err) => console.error('Failed to load guest request counters:', err));
|
| 91 |
cleanupExpired();
|
| 92 |
setInterval(() => {
|
| 93 |
cleanupExpired();
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
export async function consumeGuestRequest(ip) {
|
| 98 |
+
if (isPostgresStorageMode()) {
|
| 99 |
+
return withPgTransaction(async (client) => {
|
| 100 |
+
const now = Date.now();
|
| 101 |
+
const lookup = requestLookup(ip);
|
| 102 |
+
const { rows } = await client.query(
|
| 103 |
+
'SELECT payload FROM guest_request_counters WHERE key_lookup = $1 FOR UPDATE',
|
| 104 |
+
[lookup]
|
| 105 |
+
);
|
| 106 |
+
|
| 107 |
+
const payload = rows[0]
|
| 108 |
+
? decryptJsonPayload(rows[0].payload, 'guest-request-row')
|
| 109 |
+
: null;
|
| 110 |
+
|
| 111 |
+
let entry = payload && payload.resetAt > now
|
| 112 |
+
? { count: payload.count || 0, resetAt: payload.resetAt }
|
| 113 |
+
: { count: 0, resetAt: now + WINDOW_MS };
|
| 114 |
+
|
| 115 |
+
if (entry.count >= MAX_LOGGED_OUT_REQUESTS) {
|
| 116 |
+
ipCounters.set(ip, entry);
|
| 117 |
+
return false;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
entry = { ...entry, count: entry.count + 1 };
|
| 121 |
+
ipCounters.set(ip, entry);
|
| 122 |
+
|
| 123 |
+
await client.query(
|
| 124 |
+
`INSERT INTO guest_request_counters (key_lookup, expires_at, updated_at, payload)
|
| 125 |
+
VALUES ($1, $2, $3, $4::jsonb)
|
| 126 |
+
ON CONFLICT (key_lookup)
|
| 127 |
+
DO UPDATE SET expires_at = EXCLUDED.expires_at, updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 128 |
+
[
|
| 129 |
+
lookup,
|
| 130 |
+
new Date(entry.resetAt).toISOString(),
|
| 131 |
+
new Date().toISOString(),
|
| 132 |
+
JSON.stringify(encryptJsonPayload({ ip, ...entry }, 'guest-request-row')),
|
| 133 |
+
]
|
| 134 |
+
);
|
| 135 |
+
|
| 136 |
+
return true;
|
| 137 |
+
});
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
await loadGuestCounters();
|
| 141 |
const now = Date.now();
|
| 142 |
let entry = ipCounters.get(ip);
|
|
|
|
| 148 |
return false;
|
| 149 |
}
|
| 150 |
entry.count += 1;
|
| 151 |
+
await saveGuestCounters().catch((err) => console.error('Failed to save guest request counters:', err));
|
| 152 |
return true;
|
| 153 |
}
|
| 154 |
|
server/handleFeedback.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import path from 'path';
|
| 2 |
import crypto from 'crypto';
|
| 3 |
import { saveEncryptedJson, loadEncryptedJson } from './cryptoUtils.js';
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const DATA_DIR = '/data';
|
| 6 |
const TICKETS_FILE = path.join(DATA_DIR, 'feedback_tickets.json');
|
|
@@ -19,12 +21,47 @@ function sanitizeString(value, maxLength) {
|
|
| 19 |
}
|
| 20 |
|
| 21 |
async function loadTickets() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const data = await loadEncryptedJson(TICKETS_FILE, FEEDBACK_AAD);
|
| 23 |
if (!data || !Array.isArray(data.tickets)) return { tickets: [] };
|
| 24 |
return data;
|
| 25 |
}
|
| 26 |
|
| 27 |
async function saveTickets(data) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
await saveEncryptedJson(TICKETS_FILE, data, FEEDBACK_AAD);
|
| 29 |
}
|
| 30 |
|
|
@@ -159,4 +196,4 @@ export function registerFeedbackRoutes(app, { requireAdminTurnstile, verifyLimit
|
|
| 159 |
return res.status(500).json({ error: 'feedback:server_error' });
|
| 160 |
}
|
| 161 |
});
|
| 162 |
-
}
|
|
|
|
| 1 |
import path from 'path';
|
| 2 |
import crypto from 'crypto';
|
| 3 |
import { saveEncryptedJson, loadEncryptedJson } from './cryptoUtils.js';
|
| 4 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 5 |
+
import { decryptJsonPayload, encryptJsonPayload, pgQuery } from './postgres.js';
|
| 6 |
|
| 7 |
const DATA_DIR = '/data';
|
| 8 |
const TICKETS_FILE = path.join(DATA_DIR, 'feedback_tickets.json');
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
async function loadTickets() {
|
| 24 |
+
if (isPostgresStorageMode()) {
|
| 25 |
+
const { rows } = await pgQuery(
|
| 26 |
+
'SELECT id, payload FROM feedback_tickets ORDER BY submitted_at DESC'
|
| 27 |
+
);
|
| 28 |
+
return {
|
| 29 |
+
tickets: rows
|
| 30 |
+
.map((row) => decryptJsonPayload(row.payload, `feedback:${row.id}`))
|
| 31 |
+
.filter(Boolean),
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
const data = await loadEncryptedJson(TICKETS_FILE, FEEDBACK_AAD);
|
| 35 |
if (!data || !Array.isArray(data.tickets)) return { tickets: [] };
|
| 36 |
return data;
|
| 37 |
}
|
| 38 |
|
| 39 |
async function saveTickets(data) {
|
| 40 |
+
if (isPostgresStorageMode()) {
|
| 41 |
+
const tickets = Array.isArray(data?.tickets) ? data.tickets : [];
|
| 42 |
+
const seenIds = tickets.map((ticket) => ticket.id);
|
| 43 |
+
if (!seenIds.length) {
|
| 44 |
+
await pgQuery('DELETE FROM feedback_tickets');
|
| 45 |
+
return;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
await pgQuery('DELETE FROM feedback_tickets WHERE id <> ALL($1::text[])', [seenIds]);
|
| 49 |
+
for (const ticket of tickets) {
|
| 50 |
+
await pgQuery(
|
| 51 |
+
`INSERT INTO feedback_tickets (id, status, submitted_at, payload)
|
| 52 |
+
VALUES ($1, $2, $3, $4::jsonb)
|
| 53 |
+
ON CONFLICT (id)
|
| 54 |
+
DO UPDATE SET status = EXCLUDED.status, submitted_at = EXCLUDED.submitted_at, payload = EXCLUDED.payload`,
|
| 55 |
+
[
|
| 56 |
+
ticket.id,
|
| 57 |
+
ticket.status || 'open',
|
| 58 |
+
ticket.submittedAt,
|
| 59 |
+
JSON.stringify(encryptJsonPayload(ticket, `feedback:${ticket.id}`)),
|
| 60 |
+
]
|
| 61 |
+
);
|
| 62 |
+
}
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
await saveEncryptedJson(TICKETS_FILE, data, FEEDBACK_AAD);
|
| 66 |
}
|
| 67 |
|
|
|
|
| 196 |
return res.status(500).json({ error: 'feedback:server_error' });
|
| 197 |
}
|
| 198 |
});
|
| 199 |
+
}
|
server/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { SUPABASE_URL, SUPABASE_ANON_KEY } from './config.js';
|
|
| 15 |
import { safeSend } from './helpers.js';
|
| 16 |
import { verifySupabaseToken } from './auth.js';
|
| 17 |
import { mediaStore } from './mediaStore.js';
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
export { SUPABASE_URL, SUPABASE_ANON_KEY };
|
| 20 |
export { LIGHTNING_BASE, PUBLIC_URL } from './config.js';
|
|
@@ -58,10 +61,13 @@ const verifyLimiter = rateLimit({
|
|
| 58 |
},
|
| 59 |
});
|
| 60 |
|
| 61 |
-
const DATA_DIR = "/data";
|
| 62 |
-
const VERSION_FILE = path.join(DATA_DIR, 'version.json');
|
| 63 |
const PUBLIC_URL = process.env.PUBLIC_URL || 'default';
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
function getRequestIp(req) {
|
| 66 |
return (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
| 67 |
|| req.socket?.remoteAddress
|
|
@@ -157,64 +163,6 @@ async function verifyTurnstileToken(token, remoteIp) {
|
|
| 157 |
}
|
| 158 |
|
| 159 |
|
| 160 |
-
function loadStoredSHA() {
|
| 161 |
-
try {
|
| 162 |
-
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
| 163 |
-
if (!fs.existsSync(VERSION_FILE)) return null;
|
| 164 |
-
const data = JSON.parse(fs.readFileSync(VERSION_FILE, 'utf-8'));
|
| 165 |
-
|
| 166 |
-
// Find SHA for this PUBLIC_URL
|
| 167 |
-
const obj = data.find(item => item[PUBLIC_URL]);
|
| 168 |
-
return obj ? obj[PUBLIC_URL] : null;
|
| 169 |
-
} catch (e) {
|
| 170 |
-
console.error('Failed to load stored SHA:', e);
|
| 171 |
-
return null;
|
| 172 |
-
}
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
function saveStoredSHA(sha) {
|
| 177 |
-
try {
|
| 178 |
-
if (!fs.existsSync(DATA_DIR)) {
|
| 179 |
-
fs.mkdirSync(DATA_DIR, { recursive: true });
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
let data = [];
|
| 183 |
-
|
| 184 |
-
if (fs.existsSync(VERSION_FILE)) {
|
| 185 |
-
try {
|
| 186 |
-
data = JSON.parse(fs.readFileSync(VERSION_FILE, 'utf-8'));
|
| 187 |
-
if (!Array.isArray(data)) data = [];
|
| 188 |
-
} catch {
|
| 189 |
-
data = [];
|
| 190 |
-
}
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
let found = false;
|
| 194 |
-
|
| 195 |
-
for (const entry of data) {
|
| 196 |
-
if (entry[PUBLIC_URL]) {
|
| 197 |
-
entry[PUBLIC_URL] = sha;
|
| 198 |
-
found = true;
|
| 199 |
-
break;
|
| 200 |
-
}
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
if (!found) {
|
| 204 |
-
data.push({ [PUBLIC_URL]: sha });
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
fs.writeFileSync(
|
| 208 |
-
VERSION_FILE,
|
| 209 |
-
JSON.stringify(data, null, 2),
|
| 210 |
-
'utf-8'
|
| 211 |
-
);
|
| 212 |
-
|
| 213 |
-
} catch (e) {
|
| 214 |
-
console.error('Failed to save SHA:', e);
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
app.use(express.json({ limit: '10mb' }));
|
| 219 |
|
| 220 |
// --- API Turnstile Protection ---
|
|
@@ -527,17 +475,17 @@ async function fetchLatestSHA() {
|
|
| 527 |
const data = await res.json();
|
| 528 |
latestSHA = data.sha;
|
| 529 |
console.log(`[${PUBLIC_URL}] Updated latest SHA: ${latestSHA}`);
|
| 530 |
-
saveStoredSHA(latestSHA);
|
| 531 |
} catch (e) {
|
| 532 |
console.error('Failed to fetch latest commit SHA', e);
|
| 533 |
}
|
| 534 |
}
|
| 535 |
|
| 536 |
-
latestSHA = loadStoredSHA();
|
| 537 |
if (!latestSHA) {
|
| 538 |
console.log(`[${PUBLIC_URL}] No stored SHA found.`);
|
| 539 |
latestSHA = "latest";
|
| 540 |
-
saveStoredSHA(latestSHA);
|
| 541 |
} else {
|
| 542 |
console.log(`[${PUBLIC_URL}] Using stored SHA: ${latestSHA}`);
|
| 543 |
}
|
|
@@ -615,7 +563,7 @@ app.get('/admin/refresh', requireAdminTurnstile, verifyLimiter, async (req, res)
|
|
| 615 |
return res.status(400).send('Invalid SHA');
|
| 616 |
}
|
| 617 |
latestSHA = sha;
|
| 618 |
-
saveStoredSHA(latestSHA);
|
| 619 |
logAdminEvent(req, 'set_sha', { requestedSha: latestSHA });
|
| 620 |
console.log(`[${PUBLIC_URL}] Manual SHA set by admin: ${latestSHA}`);
|
| 621 |
return res.send(`Version set to commit ${latestSHA}`);
|
|
|
|
| 15 |
import { safeSend } from './helpers.js';
|
| 16 |
import { verifySupabaseToken } from './auth.js';
|
| 17 |
import { mediaStore } from './mediaStore.js';
|
| 18 |
+
import { initializePostgresStorage } from './postgres.js';
|
| 19 |
+
import { POSTGRES_STORAGE_DIR, STORAGE_MODE } from './dataPaths.js';
|
| 20 |
+
import { loadStoredSHA, saveStoredSHA } from './versionStore.js';
|
| 21 |
|
| 22 |
export { SUPABASE_URL, SUPABASE_ANON_KEY };
|
| 23 |
export { LIGHTNING_BASE, PUBLIC_URL } from './config.js';
|
|
|
|
| 61 |
},
|
| 62 |
});
|
| 63 |
|
|
|
|
|
|
|
| 64 |
const PUBLIC_URL = process.env.PUBLIC_URL || 'default';
|
| 65 |
|
| 66 |
+
if (STORAGE_MODE === 'postgres') {
|
| 67 |
+
await initializePostgresStorage();
|
| 68 |
+
}
|
| 69 |
+
console.log(`[storage] mode=${STORAGE_MODE} target=${STORAGE_MODE === 'postgres' ? POSTGRES_STORAGE_DIR : '/data'}`);
|
| 70 |
+
|
| 71 |
function getRequestIp(req) {
|
| 72 |
return (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
| 73 |
|| req.socket?.remoteAddress
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
app.use(express.json({ limit: '10mb' }));
|
| 167 |
|
| 168 |
// --- API Turnstile Protection ---
|
|
|
|
| 475 |
const data = await res.json();
|
| 476 |
latestSHA = data.sha;
|
| 477 |
console.log(`[${PUBLIC_URL}] Updated latest SHA: ${latestSHA}`);
|
| 478 |
+
await saveStoredSHA(PUBLIC_URL, latestSHA);
|
| 479 |
} catch (e) {
|
| 480 |
console.error('Failed to fetch latest commit SHA', e);
|
| 481 |
}
|
| 482 |
}
|
| 483 |
|
| 484 |
+
latestSHA = await loadStoredSHA(PUBLIC_URL);
|
| 485 |
if (!latestSHA) {
|
| 486 |
console.log(`[${PUBLIC_URL}] No stored SHA found.`);
|
| 487 |
latestSHA = "latest";
|
| 488 |
+
await saveStoredSHA(PUBLIC_URL, latestSHA);
|
| 489 |
} else {
|
| 490 |
console.log(`[${PUBLIC_URL}] Using stored SHA: ${latestSHA}`);
|
| 491 |
}
|
|
|
|
| 563 |
return res.status(400).send('Invalid SHA');
|
| 564 |
}
|
| 565 |
latestSHA = sha;
|
| 566 |
+
await saveStoredSHA(PUBLIC_URL, latestSHA);
|
| 567 |
logAdminEvent(req, 'set_sha', { requestedSha: latestSHA });
|
| 568 |
console.log(`[${PUBLIC_URL}] Manual SHA set by admin: ${latestSHA}`);
|
| 569 |
return res.send(`Version set to commit ${latestSHA}`);
|
server/mediaStore.js
CHANGED
|
@@ -2,6 +2,16 @@ import crypto from 'crypto';
|
|
| 2 |
import fs from 'fs/promises';
|
| 3 |
import path from 'path';
|
| 4 |
import { loadEncryptedJson, saveEncryptedJson, readEncryptedFile, writeEncryptedFile } from './cryptoUtils.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const DATA_ROOT = '/data/media';
|
| 7 |
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
|
|
@@ -81,8 +91,29 @@ function guessMimeType(name, fallbackKind = 'file') {
|
|
| 81 |
}
|
| 82 |
}
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
async function ensureLoaded() {
|
| 85 |
if (state.loaded) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const stored = await loadEncryptedJson(INDEX_FILE);
|
| 87 |
state.index = {
|
| 88 |
entries: stored?.entries || {},
|
|
@@ -91,7 +122,59 @@ async function ensureLoaded() {
|
|
| 91 |
await purgeExpiredInternal();
|
| 92 |
}
|
| 93 |
|
| 94 |
-
async function saveIndex() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
await saveEncryptedJson(INDEX_FILE, state.index);
|
| 96 |
}
|
| 97 |
|
|
@@ -132,6 +215,38 @@ function buildAad(entry) {
|
|
| 132 |
return `media:${entry.id}:${entry.ownerType}:${entry.ownerId}`;
|
| 133 |
}
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
function isOwnedFile(entry, owner) {
|
| 136 |
return entry?.type === 'file' && entry.ownerType === owner.type && entry.ownerId === owner.id;
|
| 137 |
}
|
|
@@ -210,10 +325,15 @@ function assertQuotaAvailable(owner, additionalBytes = 0) {
|
|
| 210 |
async function purgeEntry(id) {
|
| 211 |
const entry = getEntry(id);
|
| 212 |
if (!entry) return;
|
| 213 |
-
if (entry.type === 'file') {
|
| 214 |
-
await
|
| 215 |
}
|
| 216 |
delete state.index.entries[id];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
async function purgeExpiredInternal() {
|
|
@@ -229,7 +349,7 @@ async function purgeExpiredInternal() {
|
|
| 229 |
}
|
| 230 |
changed = true;
|
| 231 |
}
|
| 232 |
-
if (changed) await saveIndex();
|
| 233 |
}
|
| 234 |
|
| 235 |
setInterval(() => {
|
|
@@ -308,9 +428,16 @@ export const mediaStore = {
|
|
| 308 |
deletedByAssistant,
|
| 309 |
};
|
| 310 |
|
| 311 |
-
await writeEncryptedFile(blobPathFor(entry.id), Buffer.from(buffer), buildAad(entry));
|
| 312 |
state.index.entries[entry.id] = entry;
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
return sanitizeEntry(entry);
|
| 315 |
},
|
| 316 |
|
|
@@ -335,7 +462,7 @@ export const mediaStore = {
|
|
| 335 |
};
|
| 336 |
|
| 337 |
state.index.entries[folder.id] = folder;
|
| 338 |
-
await saveIndex();
|
| 339 |
return sanitizeEntry(folder);
|
| 340 |
},
|
| 341 |
|
|
@@ -369,7 +496,7 @@ export const mediaStore = {
|
|
| 369 |
if (!entry || entry.type !== 'file' || !canAccess(entry, owner)) return null;
|
| 370 |
return {
|
| 371 |
entry: sanitizeEntry(entry),
|
| 372 |
-
buffer: await
|
| 373 |
};
|
| 374 |
},
|
| 375 |
|
|
@@ -397,8 +524,15 @@ export const mediaStore = {
|
|
| 397 |
entry.size = nextSize;
|
| 398 |
entry.updatedAt = nowIso();
|
| 399 |
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
return sanitizeEntry(entry);
|
| 403 |
},
|
| 404 |
|
|
@@ -409,7 +543,7 @@ export const mediaStore = {
|
|
| 409 |
if (!entry || !canAccess(entry, owner)) return null;
|
| 410 |
entry.name = normalizeName(name, entry.name || 'Untitled');
|
| 411 |
entry.updatedAt = nowIso();
|
| 412 |
-
await saveIndex();
|
| 413 |
return sanitizeEntry(entry);
|
| 414 |
},
|
| 415 |
|
|
@@ -419,6 +553,7 @@ export const mediaStore = {
|
|
| 419 |
const destinationId = parentId ? resolveParentFolder(owner, parentId) : null;
|
| 420 |
if (parentId && !destinationId) throw new Error('Invalid destination folder');
|
| 421 |
const updated = [];
|
|
|
|
| 422 |
for (const id of ids || []) {
|
| 423 |
const entry = getEntry(id);
|
| 424 |
if (!entry || !canAccess(entry, owner)) continue;
|
|
@@ -427,8 +562,9 @@ export const mediaStore = {
|
|
| 427 |
entry.parentId = destinationId;
|
| 428 |
entry.updatedAt = nowIso();
|
| 429 |
updated.push(sanitizeEntry(entry));
|
|
|
|
| 430 |
}
|
| 431 |
-
if (
|
| 432 |
return updated;
|
| 433 |
},
|
| 434 |
|
|
@@ -436,6 +572,7 @@ export const mediaStore = {
|
|
| 436 |
ensureOwner(owner);
|
| 437 |
await ensureLoaded();
|
| 438 |
const trashed = [];
|
|
|
|
| 439 |
const now = nowIso();
|
| 440 |
const purgeAt = new Date(Date.now() + TRASH_RETENTION_MS).toISOString();
|
| 441 |
|
|
@@ -449,9 +586,10 @@ export const mediaStore = {
|
|
| 449 |
target.purgeAt = purgeAt;
|
| 450 |
target.updatedAt = now;
|
| 451 |
trashed.push(sanitizeEntry(target));
|
|
|
|
| 452 |
}
|
| 453 |
}
|
| 454 |
-
if (
|
| 455 |
return trashed;
|
| 456 |
},
|
| 457 |
|
|
@@ -459,6 +597,7 @@ export const mediaStore = {
|
|
| 459 |
ensureOwner(owner);
|
| 460 |
await ensureLoaded();
|
| 461 |
const restored = [];
|
|
|
|
| 462 |
for (const id of ids || []) {
|
| 463 |
const entry = getEntry(id);
|
| 464 |
if (!entry || !canAccess(entry, owner)) continue;
|
|
@@ -469,9 +608,10 @@ export const mediaStore = {
|
|
| 469 |
target.purgeAt = null;
|
| 470 |
target.updatedAt = nowIso();
|
| 471 |
restored.push(sanitizeEntry(target));
|
|
|
|
| 472 |
}
|
| 473 |
}
|
| 474 |
-
if (
|
| 475 |
return restored;
|
| 476 |
},
|
| 477 |
|
|
@@ -489,7 +629,7 @@ export const mediaStore = {
|
|
| 489 |
for (const targetId of removedIds) {
|
| 490 |
await purgeEntry(targetId);
|
| 491 |
}
|
| 492 |
-
if (removedIds.size) await saveIndex();
|
| 493 |
return [...removedIds];
|
| 494 |
},
|
| 495 |
|
|
@@ -498,14 +638,16 @@ export const mediaStore = {
|
|
| 498 |
if (!sessionId) return [];
|
| 499 |
await ensureLoaded();
|
| 500 |
const updated = [];
|
|
|
|
| 501 |
for (const id of ids || []) {
|
| 502 |
const entry = getEntry(id);
|
| 503 |
if (!entry || !canAccess(entry, owner)) continue;
|
| 504 |
entry.sessionIds = [...new Set([...(entry.sessionIds || []), sessionId])];
|
| 505 |
entry.updatedAt = nowIso();
|
| 506 |
updated.push(sanitizeEntry(entry));
|
|
|
|
| 507 |
}
|
| 508 |
-
if (
|
| 509 |
return updated;
|
| 510 |
},
|
| 511 |
|
|
|
|
| 2 |
import fs from 'fs/promises';
|
| 3 |
import path from 'path';
|
| 4 |
import { loadEncryptedJson, saveEncryptedJson, readEncryptedFile, writeEncryptedFile } from './cryptoUtils.js';
|
| 5 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 6 |
+
import {
|
| 7 |
+
decryptBinaryPayload,
|
| 8 |
+
decryptJsonPayload,
|
| 9 |
+
encryptBinaryPayload,
|
| 10 |
+
encryptJsonPayload,
|
| 11 |
+
makeOwnerLookup,
|
| 12 |
+
pgQuery,
|
| 13 |
+
withPgTransaction,
|
| 14 |
+
} from './postgres.js';
|
| 15 |
|
| 16 |
const DATA_ROOT = '/data/media';
|
| 17 |
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
|
|
|
|
| 91 |
}
|
| 92 |
}
|
| 93 |
|
| 94 |
+
function mediaEntryAad(id) {
|
| 95 |
+
return `media-entry:${id}`;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
async function loadSqlEntries() {
|
| 99 |
+
const { rows } = await pgQuery('SELECT id, payload FROM media_entries');
|
| 100 |
+
const entries = {};
|
| 101 |
+
for (const row of rows) {
|
| 102 |
+
const entry = decryptJsonPayload(row.payload, mediaEntryAad(row.id));
|
| 103 |
+
if (entry?.id) entries[entry.id] = entry;
|
| 104 |
+
}
|
| 105 |
+
return entries;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
async function ensureLoaded() {
|
| 109 |
if (state.loaded) return;
|
| 110 |
+
if (isPostgresStorageMode()) {
|
| 111 |
+
state.index = { entries: await loadSqlEntries() };
|
| 112 |
+
state.loaded = true;
|
| 113 |
+
await purgeExpiredInternal();
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
const stored = await loadEncryptedJson(INDEX_FILE);
|
| 118 |
state.index = {
|
| 119 |
entries: stored?.entries || {},
|
|
|
|
| 122 |
await purgeExpiredInternal();
|
| 123 |
}
|
| 124 |
|
| 125 |
+
async function saveIndex(changedIds = null, deletedIds = [], client = null) {
|
| 126 |
+
if (isPostgresStorageMode()) {
|
| 127 |
+
const idsToPersist = changedIds ? [...new Set(changedIds)] : Object.keys(state.index.entries);
|
| 128 |
+
const idsToDelete = [...new Set(deletedIds || [])];
|
| 129 |
+
const run = async (runner) => {
|
| 130 |
+
for (const id of idsToDelete) {
|
| 131 |
+
await runner.query('DELETE FROM media_entries WHERE id = $1', [id]);
|
| 132 |
+
}
|
| 133 |
+
for (const id of idsToPersist) {
|
| 134 |
+
const entry = state.index.entries[id];
|
| 135 |
+
if (!entry) continue;
|
| 136 |
+
await runner.query(
|
| 137 |
+
`INSERT INTO media_entries (
|
| 138 |
+
id, owner_lookup, parent_id, entry_type, updated_at, created_at,
|
| 139 |
+
trashed_at, purge_at, expires_at, size_bytes, payload
|
| 140 |
+
)
|
| 141 |
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb)
|
| 142 |
+
ON CONFLICT (id)
|
| 143 |
+
DO UPDATE SET
|
| 144 |
+
owner_lookup = EXCLUDED.owner_lookup,
|
| 145 |
+
parent_id = EXCLUDED.parent_id,
|
| 146 |
+
entry_type = EXCLUDED.entry_type,
|
| 147 |
+
updated_at = EXCLUDED.updated_at,
|
| 148 |
+
created_at = EXCLUDED.created_at,
|
| 149 |
+
trashed_at = EXCLUDED.trashed_at,
|
| 150 |
+
purge_at = EXCLUDED.purge_at,
|
| 151 |
+
expires_at = EXCLUDED.expires_at,
|
| 152 |
+
size_bytes = EXCLUDED.size_bytes,
|
| 153 |
+
payload = EXCLUDED.payload`,
|
| 154 |
+
[
|
| 155 |
+
entry.id,
|
| 156 |
+
makeOwnerLookup({ type: entry.ownerType, id: entry.ownerId }),
|
| 157 |
+
entry.parentId || null,
|
| 158 |
+
entry.type,
|
| 159 |
+
entry.updatedAt || entry.createdAt,
|
| 160 |
+
entry.createdAt,
|
| 161 |
+
entry.trashedAt || null,
|
| 162 |
+
entry.purgeAt || null,
|
| 163 |
+
entry.expiresAt || null,
|
| 164 |
+
entry.size || 0,
|
| 165 |
+
JSON.stringify(encryptJsonPayload(entry, mediaEntryAad(entry.id))),
|
| 166 |
+
]
|
| 167 |
+
);
|
| 168 |
+
}
|
| 169 |
+
};
|
| 170 |
+
if (client) {
|
| 171 |
+
await run(client);
|
| 172 |
+
} else {
|
| 173 |
+
await withPgTransaction(run);
|
| 174 |
+
}
|
| 175 |
+
return;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
await saveEncryptedJson(INDEX_FILE, state.index);
|
| 179 |
}
|
| 180 |
|
|
|
|
| 215 |
return `media:${entry.id}:${entry.ownerType}:${entry.ownerId}`;
|
| 216 |
}
|
| 217 |
|
| 218 |
+
async function saveBlob(entry, buffer, client = null) {
|
| 219 |
+
if (isPostgresStorageMode()) {
|
| 220 |
+
const runner = client || { query: pgQuery };
|
| 221 |
+
await runner.query(
|
| 222 |
+
`INSERT INTO media_blobs (entry_id, updated_at, payload)
|
| 223 |
+
VALUES ($1, $2, $3)
|
| 224 |
+
ON CONFLICT (entry_id)
|
| 225 |
+
DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 226 |
+
[entry.id, entry.updatedAt || nowIso(), encryptBinaryPayload(buffer, buildAad(entry))]
|
| 227 |
+
);
|
| 228 |
+
return;
|
| 229 |
+
}
|
| 230 |
+
await writeEncryptedFile(blobPathFor(entry.id), Buffer.from(buffer), buildAad(entry));
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
async function loadBlob(entry) {
|
| 234 |
+
if (isPostgresStorageMode()) {
|
| 235 |
+
const { rows } = await pgQuery('SELECT payload FROM media_blobs WHERE entry_id = $1', [entry.id]);
|
| 236 |
+
if (!rows[0]) throw new Error(`Missing blob for media entry ${entry.id}`);
|
| 237 |
+
return decryptBinaryPayload(rows[0].payload, buildAad(entry));
|
| 238 |
+
}
|
| 239 |
+
return readEncryptedFile(blobPathFor(entry.id), buildAad(entry));
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
async function deleteBlob(id) {
|
| 243 |
+
if (isPostgresStorageMode()) {
|
| 244 |
+
await pgQuery('DELETE FROM media_blobs WHERE entry_id = $1', [id]);
|
| 245 |
+
return;
|
| 246 |
+
}
|
| 247 |
+
await fs.rm(blobPathFor(id), { force: true }).catch(() => {});
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
function isOwnedFile(entry, owner) {
|
| 251 |
return entry?.type === 'file' && entry.ownerType === owner.type && entry.ownerId === owner.id;
|
| 252 |
}
|
|
|
|
| 325 |
async function purgeEntry(id) {
|
| 326 |
const entry = getEntry(id);
|
| 327 |
if (!entry) return;
|
| 328 |
+
if (entry.type === 'file' && !isPostgresStorageMode()) {
|
| 329 |
+
await deleteBlob(id);
|
| 330 |
}
|
| 331 |
delete state.index.entries[id];
|
| 332 |
+
if (isPostgresStorageMode()) {
|
| 333 |
+
await saveIndex([], [id]);
|
| 334 |
+
return;
|
| 335 |
+
}
|
| 336 |
+
if (entry.type === 'file') await deleteBlob(id);
|
| 337 |
}
|
| 338 |
|
| 339 |
async function purgeExpiredInternal() {
|
|
|
|
| 349 |
}
|
| 350 |
changed = true;
|
| 351 |
}
|
| 352 |
+
if (changed && !isPostgresStorageMode()) await saveIndex();
|
| 353 |
}
|
| 354 |
|
| 355 |
setInterval(() => {
|
|
|
|
| 428 |
deletedByAssistant,
|
| 429 |
};
|
| 430 |
|
|
|
|
| 431 |
state.index.entries[entry.id] = entry;
|
| 432 |
+
if (isPostgresStorageMode()) {
|
| 433 |
+
await withPgTransaction(async (client) => {
|
| 434 |
+
await saveBlob(entry, Buffer.from(buffer), client);
|
| 435 |
+
await saveIndex([entry.id], [], client);
|
| 436 |
+
});
|
| 437 |
+
} else {
|
| 438 |
+
await saveBlob(entry, Buffer.from(buffer));
|
| 439 |
+
await saveIndex([entry.id]);
|
| 440 |
+
}
|
| 441 |
return sanitizeEntry(entry);
|
| 442 |
},
|
| 443 |
|
|
|
|
| 462 |
};
|
| 463 |
|
| 464 |
state.index.entries[folder.id] = folder;
|
| 465 |
+
await saveIndex([folder.id]);
|
| 466 |
return sanitizeEntry(folder);
|
| 467 |
},
|
| 468 |
|
|
|
|
| 496 |
if (!entry || entry.type !== 'file' || !canAccess(entry, owner)) return null;
|
| 497 |
return {
|
| 498 |
entry: sanitizeEntry(entry),
|
| 499 |
+
buffer: await loadBlob(entry),
|
| 500 |
};
|
| 501 |
},
|
| 502 |
|
|
|
|
| 524 |
entry.size = nextSize;
|
| 525 |
entry.updatedAt = nowIso();
|
| 526 |
|
| 527 |
+
if (isPostgresStorageMode()) {
|
| 528 |
+
await withPgTransaction(async (client) => {
|
| 529 |
+
await saveBlob(entry, Buffer.from(buffer), client);
|
| 530 |
+
await saveIndex([entry.id], [], client);
|
| 531 |
+
});
|
| 532 |
+
} else {
|
| 533 |
+
await saveBlob(entry, Buffer.from(buffer));
|
| 534 |
+
await saveIndex([entry.id]);
|
| 535 |
+
}
|
| 536 |
return sanitizeEntry(entry);
|
| 537 |
},
|
| 538 |
|
|
|
|
| 543 |
if (!entry || !canAccess(entry, owner)) return null;
|
| 544 |
entry.name = normalizeName(name, entry.name || 'Untitled');
|
| 545 |
entry.updatedAt = nowIso();
|
| 546 |
+
await saveIndex([entry.id]);
|
| 547 |
return sanitizeEntry(entry);
|
| 548 |
},
|
| 549 |
|
|
|
|
| 553 |
const destinationId = parentId ? resolveParentFolder(owner, parentId) : null;
|
| 554 |
if (parentId && !destinationId) throw new Error('Invalid destination folder');
|
| 555 |
const updated = [];
|
| 556 |
+
const changedIds = new Set();
|
| 557 |
for (const id of ids || []) {
|
| 558 |
const entry = getEntry(id);
|
| 559 |
if (!entry || !canAccess(entry, owner)) continue;
|
|
|
|
| 562 |
entry.parentId = destinationId;
|
| 563 |
entry.updatedAt = nowIso();
|
| 564 |
updated.push(sanitizeEntry(entry));
|
| 565 |
+
changedIds.add(entry.id);
|
| 566 |
}
|
| 567 |
+
if (changedIds.size) await saveIndex([...changedIds]);
|
| 568 |
return updated;
|
| 569 |
},
|
| 570 |
|
|
|
|
| 572 |
ensureOwner(owner);
|
| 573 |
await ensureLoaded();
|
| 574 |
const trashed = [];
|
| 575 |
+
const changedIds = new Set();
|
| 576 |
const now = nowIso();
|
| 577 |
const purgeAt = new Date(Date.now() + TRASH_RETENTION_MS).toISOString();
|
| 578 |
|
|
|
|
| 586 |
target.purgeAt = purgeAt;
|
| 587 |
target.updatedAt = now;
|
| 588 |
trashed.push(sanitizeEntry(target));
|
| 589 |
+
changedIds.add(target.id);
|
| 590 |
}
|
| 591 |
}
|
| 592 |
+
if (changedIds.size) await saveIndex([...changedIds]);
|
| 593 |
return trashed;
|
| 594 |
},
|
| 595 |
|
|
|
|
| 597 |
ensureOwner(owner);
|
| 598 |
await ensureLoaded();
|
| 599 |
const restored = [];
|
| 600 |
+
const changedIds = new Set();
|
| 601 |
for (const id of ids || []) {
|
| 602 |
const entry = getEntry(id);
|
| 603 |
if (!entry || !canAccess(entry, owner)) continue;
|
|
|
|
| 608 |
target.purgeAt = null;
|
| 609 |
target.updatedAt = nowIso();
|
| 610 |
restored.push(sanitizeEntry(target));
|
| 611 |
+
changedIds.add(target.id);
|
| 612 |
}
|
| 613 |
}
|
| 614 |
+
if (changedIds.size) await saveIndex([...changedIds]);
|
| 615 |
return restored;
|
| 616 |
},
|
| 617 |
|
|
|
|
| 629 |
for (const targetId of removedIds) {
|
| 630 |
await purgeEntry(targetId);
|
| 631 |
}
|
| 632 |
+
if (removedIds.size && !isPostgresStorageMode()) await saveIndex();
|
| 633 |
return [...removedIds];
|
| 634 |
},
|
| 635 |
|
|
|
|
| 638 |
if (!sessionId) return [];
|
| 639 |
await ensureLoaded();
|
| 640 |
const updated = [];
|
| 641 |
+
const changedIds = new Set();
|
| 642 |
for (const id of ids || []) {
|
| 643 |
const entry = getEntry(id);
|
| 644 |
if (!entry || !canAccess(entry, owner)) continue;
|
| 645 |
entry.sessionIds = [...new Set([...(entry.sessionIds || []), sessionId])];
|
| 646 |
entry.updatedAt = nowIso();
|
| 647 |
updated.push(sanitizeEntry(entry));
|
| 648 |
+
changedIds.add(entry.id);
|
| 649 |
}
|
| 650 |
+
if (changedIds.size) await saveIndex([...changedIds]);
|
| 651 |
return updated;
|
| 652 |
},
|
| 653 |
|
server/memoryStore.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
| 1 |
import crypto from 'crypto';
|
| 2 |
import path from 'path';
|
| 3 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const DATA_ROOT = '/data/memories';
|
| 6 |
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
|
|
@@ -26,8 +33,12 @@ function sanitizeText(text) {
|
|
| 26 |
return String(text || '').replace(/\s+/g, ' ').trim().slice(0, MAX_MEMORY_LENGTH);
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
async function ensureLoaded() {
|
| 30 |
-
if (state.loaded) return;
|
| 31 |
const stored = await loadEncryptedJson(INDEX_FILE);
|
| 32 |
state.index = {
|
| 33 |
memories: stored?.memories || {},
|
|
@@ -54,9 +65,39 @@ function sanitize(memory) {
|
|
| 54 |
};
|
| 55 |
}
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
export const memoryStore = {
|
| 58 |
async list(owner) {
|
| 59 |
ensureOwner(owner);
|
|
|
|
|
|
|
| 60 |
await ensureLoaded();
|
| 61 |
return Object.values(state.index.memories)
|
| 62 |
.filter((memory) => matchesOwner(memory, owner))
|
|
@@ -66,7 +107,6 @@ export const memoryStore = {
|
|
| 66 |
|
| 67 |
async create(owner, { content, sessionId = null, source = 'assistant' }) {
|
| 68 |
ensureOwner(owner);
|
| 69 |
-
await ensureLoaded();
|
| 70 |
const normalized = sanitizeText(content);
|
| 71 |
if (!normalized) return null;
|
| 72 |
|
|
@@ -80,6 +120,13 @@ export const memoryStore = {
|
|
| 80 |
createdAt: nowIso(),
|
| 81 |
updatedAt: nowIso(),
|
| 82 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
state.index.memories[memory.id] = memory;
|
| 84 |
await saveIndex();
|
| 85 |
return sanitize(memory);
|
|
@@ -87,11 +134,25 @@ export const memoryStore = {
|
|
| 87 |
|
| 88 |
async update(owner, id, content) {
|
| 89 |
ensureOwner(owner);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
await ensureLoaded();
|
| 91 |
const memory = state.index.memories[id];
|
| 92 |
if (!memory || !matchesOwner(memory, owner)) return null;
|
| 93 |
-
const normalized = sanitizeText(content);
|
| 94 |
-
if (!normalized) return null;
|
| 95 |
memory.content = normalized;
|
| 96 |
memory.updatedAt = nowIso();
|
| 97 |
await saveIndex();
|
|
@@ -100,6 +161,14 @@ export const memoryStore = {
|
|
| 100 |
|
| 101 |
async delete(owner, id) {
|
| 102 |
ensureOwner(owner);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
await ensureLoaded();
|
| 104 |
const memory = state.index.memories[id];
|
| 105 |
if (!memory || !matchesOwner(memory, owner)) return false;
|
|
|
|
| 1 |
import crypto from 'crypto';
|
| 2 |
import path from 'path';
|
| 3 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
| 4 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 5 |
+
import {
|
| 6 |
+
decryptJsonPayload,
|
| 7 |
+
encryptJsonPayload,
|
| 8 |
+
makeOwnerLookup,
|
| 9 |
+
pgQuery,
|
| 10 |
+
} from './postgres.js';
|
| 11 |
|
| 12 |
const DATA_ROOT = '/data/memories';
|
| 13 |
const INDEX_FILE = path.join(DATA_ROOT, 'index.json');
|
|
|
|
| 33 |
return String(text || '').replace(/\s+/g, ' ').trim().slice(0, MAX_MEMORY_LENGTH);
|
| 34 |
}
|
| 35 |
|
| 36 |
+
function memoryAad(memoryId) {
|
| 37 |
+
return `memory:${memoryId}`;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
async function ensureLoaded() {
|
| 41 |
+
if (state.loaded || isPostgresStorageMode()) return;
|
| 42 |
const stored = await loadEncryptedJson(INDEX_FILE);
|
| 43 |
state.index = {
|
| 44 |
memories: stored?.memories || {},
|
|
|
|
| 65 |
};
|
| 66 |
}
|
| 67 |
|
| 68 |
+
async function listSql(owner) {
|
| 69 |
+
const { rows } = await pgQuery(
|
| 70 |
+
'SELECT id, payload, updated_at FROM memories WHERE owner_lookup = $1 ORDER BY updated_at DESC',
|
| 71 |
+
[makeOwnerLookup(owner)]
|
| 72 |
+
);
|
| 73 |
+
return rows
|
| 74 |
+
.map((row) => decryptJsonPayload(row.payload, memoryAad(row.id)))
|
| 75 |
+
.filter(Boolean)
|
| 76 |
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
| 77 |
+
.map(sanitize);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
async function upsertSql(memory) {
|
| 81 |
+
await pgQuery(
|
| 82 |
+
`INSERT INTO memories (id, owner_lookup, created_at, updated_at, payload)
|
| 83 |
+
VALUES ($1, $2, $3, $4, $5::jsonb)
|
| 84 |
+
ON CONFLICT (id)
|
| 85 |
+
DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 86 |
+
[
|
| 87 |
+
memory.id,
|
| 88 |
+
makeOwnerLookup({ type: memory.ownerType, id: memory.ownerId }),
|
| 89 |
+
memory.createdAt,
|
| 90 |
+
memory.updatedAt,
|
| 91 |
+
JSON.stringify(encryptJsonPayload(memory, memoryAad(memory.id))),
|
| 92 |
+
]
|
| 93 |
+
);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
export const memoryStore = {
|
| 97 |
async list(owner) {
|
| 98 |
ensureOwner(owner);
|
| 99 |
+
if (isPostgresStorageMode()) return listSql(owner);
|
| 100 |
+
|
| 101 |
await ensureLoaded();
|
| 102 |
return Object.values(state.index.memories)
|
| 103 |
.filter((memory) => matchesOwner(memory, owner))
|
|
|
|
| 107 |
|
| 108 |
async create(owner, { content, sessionId = null, source = 'assistant' }) {
|
| 109 |
ensureOwner(owner);
|
|
|
|
| 110 |
const normalized = sanitizeText(content);
|
| 111 |
if (!normalized) return null;
|
| 112 |
|
|
|
|
| 120 |
createdAt: nowIso(),
|
| 121 |
updatedAt: nowIso(),
|
| 122 |
};
|
| 123 |
+
|
| 124 |
+
if (isPostgresStorageMode()) {
|
| 125 |
+
await upsertSql(memory);
|
| 126 |
+
return sanitize(memory);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
await ensureLoaded();
|
| 130 |
state.index.memories[memory.id] = memory;
|
| 131 |
await saveIndex();
|
| 132 |
return sanitize(memory);
|
|
|
|
| 134 |
|
| 135 |
async update(owner, id, content) {
|
| 136 |
ensureOwner(owner);
|
| 137 |
+
const normalized = sanitizeText(content);
|
| 138 |
+
if (!normalized) return null;
|
| 139 |
+
|
| 140 |
+
if (isPostgresStorageMode()) {
|
| 141 |
+
const { rows } = await pgQuery(
|
| 142 |
+
'SELECT payload FROM memories WHERE id = $1 AND owner_lookup = $2',
|
| 143 |
+
[id, makeOwnerLookup(owner)]
|
| 144 |
+
);
|
| 145 |
+
const memory = rows[0] ? decryptJsonPayload(rows[0].payload, memoryAad(id)) : null;
|
| 146 |
+
if (!memory || !matchesOwner(memory, owner)) return null;
|
| 147 |
+
memory.content = normalized;
|
| 148 |
+
memory.updatedAt = nowIso();
|
| 149 |
+
await upsertSql(memory);
|
| 150 |
+
return sanitize(memory);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
await ensureLoaded();
|
| 154 |
const memory = state.index.memories[id];
|
| 155 |
if (!memory || !matchesOwner(memory, owner)) return null;
|
|
|
|
|
|
|
| 156 |
memory.content = normalized;
|
| 157 |
memory.updatedAt = nowIso();
|
| 158 |
await saveIndex();
|
|
|
|
| 161 |
|
| 162 |
async delete(owner, id) {
|
| 163 |
ensureOwner(owner);
|
| 164 |
+
if (isPostgresStorageMode()) {
|
| 165 |
+
const result = await pgQuery(
|
| 166 |
+
'DELETE FROM memories WHERE id = $1 AND owner_lookup = $2',
|
| 167 |
+
[id, makeOwnerLookup(owner)]
|
| 168 |
+
);
|
| 169 |
+
return result.rowCount > 0;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
await ensureLoaded();
|
| 173 |
const memory = state.index.memories[id];
|
| 174 |
if (!memory || !matchesOwner(memory, owner)) return false;
|
server/postgres.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Pool } from 'pg';
|
| 2 |
+
import {
|
| 3 |
+
createLookupHash,
|
| 4 |
+
decryptBuffer,
|
| 5 |
+
decryptJson,
|
| 6 |
+
encryptBuffer,
|
| 7 |
+
encryptJson,
|
| 8 |
+
packEncryptedBuffer,
|
| 9 |
+
unpackEncryptedBuffer,
|
| 10 |
+
} from './cryptoUtils.js';
|
| 11 |
+
import { isPostgresStorageMode, POSTGRES_STORAGE_DIR, STORAGE_MODE } from './dataPaths.js';
|
| 12 |
+
import { POSTGRES_SCHEMA_SQL } from './postgresSchema.js';
|
| 13 |
+
|
| 14 |
+
let poolInstance = null;
|
| 15 |
+
let initPromise = null;
|
| 16 |
+
|
| 17 |
+
function resolveSslConfig() {
|
| 18 |
+
const raw = String(process.env.PGSSL || process.env.PGSSLMODE || '').trim().toLowerCase();
|
| 19 |
+
if (!raw || ['false', '0', 'off', 'disable'].includes(raw)) return false;
|
| 20 |
+
return { rejectUnauthorized: false };
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export function buildPoolConfig() {
|
| 24 |
+
const connectionString = process.env.DATABASE_URL || process.env.POSTGRES_URL || '';
|
| 25 |
+
const ssl = resolveSslConfig();
|
| 26 |
+
if (connectionString) {
|
| 27 |
+
return { connectionString, ssl };
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const { PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE } = process.env;
|
| 31 |
+
if (!PGHOST || !PGUSER || !PGDATABASE) {
|
| 32 |
+
throw new Error(
|
| 33 |
+
`PostgreSQL storage is enabled by ${POSTGRES_STORAGE_DIR}, but no database connection is configured. Set DATABASE_URL or PGHOST/PGUSER/PGDATABASE.`
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
return {
|
| 38 |
+
host: PGHOST,
|
| 39 |
+
port: PGPORT ? Number(PGPORT) : 5432,
|
| 40 |
+
user: PGUSER,
|
| 41 |
+
password: PGPASSWORD || '',
|
| 42 |
+
database: PGDATABASE,
|
| 43 |
+
ssl,
|
| 44 |
+
};
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
async function createPool() {
|
| 48 |
+
return createStandalonePostgresPool();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export async function createStandalonePostgresPool() {
|
| 52 |
+
const pool = new Pool(buildPoolConfig());
|
| 53 |
+
await pool.query(POSTGRES_SCHEMA_SQL);
|
| 54 |
+
pool.on('error', (err) => {
|
| 55 |
+
console.error('PostgreSQL pool error:', err);
|
| 56 |
+
});
|
| 57 |
+
return pool;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export async function initializePostgresStorage() {
|
| 61 |
+
if (!isPostgresStorageMode()) return false;
|
| 62 |
+
if (!initPromise) {
|
| 63 |
+
initPromise = createPool().then((pool) => {
|
| 64 |
+
poolInstance = pool;
|
| 65 |
+
return pool;
|
| 66 |
+
});
|
| 67 |
+
}
|
| 68 |
+
await initPromise;
|
| 69 |
+
return true;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export async function getPostgresPool() {
|
| 73 |
+
if (!isPostgresStorageMode()) {
|
| 74 |
+
throw new Error(`PostgreSQL storage is not active (mode=${STORAGE_MODE})`);
|
| 75 |
+
}
|
| 76 |
+
await initializePostgresStorage();
|
| 77 |
+
return poolInstance;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export async function pgQuery(text, params = []) {
|
| 81 |
+
const pool = await getPostgresPool();
|
| 82 |
+
return pool.query(text, params);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export async function withPgClient(fn) {
|
| 86 |
+
const pool = await getPostgresPool();
|
| 87 |
+
const client = await pool.connect();
|
| 88 |
+
try {
|
| 89 |
+
return await fn(client);
|
| 90 |
+
} finally {
|
| 91 |
+
client.release();
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
export async function withPgTransaction(fn) {
|
| 96 |
+
return withPgClient(async (client) => {
|
| 97 |
+
await client.query('BEGIN');
|
| 98 |
+
try {
|
| 99 |
+
const result = await fn(client);
|
| 100 |
+
await client.query('COMMIT');
|
| 101 |
+
return result;
|
| 102 |
+
} catch (err) {
|
| 103 |
+
await client.query('ROLLBACK');
|
| 104 |
+
throw err;
|
| 105 |
+
}
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export function encryptJsonPayload(data, aad = '') {
|
| 110 |
+
return encryptJson(data, aad);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
export function decryptJsonPayload(payload, aad = '') {
|
| 114 |
+
return decryptJson(payload, aad);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
export function encryptBinaryPayload(buffer, aad = '') {
|
| 118 |
+
return packEncryptedBuffer(encryptBuffer(Buffer.from(buffer), aad));
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export function decryptBinaryPayload(payload, aad = '') {
|
| 122 |
+
const packed = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
| 123 |
+
return decryptBuffer(unpackEncryptedBuffer(packed), aad);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
export function makeOwnerLookup(owner) {
|
| 127 |
+
return createLookupHash(`${owner?.type || 'unknown'}:${owner?.id || ''}`, 'owner');
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
export function makeLookupToken(namespace, value) {
|
| 131 |
+
return createLookupHash(String(value ?? ''), namespace);
|
| 132 |
+
}
|
server/postgresSchema.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const POSTGRES_SCHEMA_SQL = `
|
| 2 |
+
CREATE TABLE IF NOT EXISTS app_versions (
|
| 3 |
+
public_url_lookup text PRIMARY KEY,
|
| 4 |
+
updated_at timestamptz NOT NULL,
|
| 5 |
+
payload jsonb NOT NULL
|
| 6 |
+
);
|
| 7 |
+
|
| 8 |
+
CREATE TABLE IF NOT EXISTS guest_state (
|
| 9 |
+
owner_lookup text PRIMARY KEY,
|
| 10 |
+
expires_at timestamptz,
|
| 11 |
+
updated_at timestamptz NOT NULL,
|
| 12 |
+
payload jsonb NOT NULL
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
CREATE TABLE IF NOT EXISTS chat_sessions (
|
| 16 |
+
id text PRIMARY KEY,
|
| 17 |
+
scope_type text NOT NULL,
|
| 18 |
+
owner_lookup text NOT NULL,
|
| 19 |
+
created_at timestamptz NOT NULL,
|
| 20 |
+
updated_at timestamptz NOT NULL,
|
| 21 |
+
expires_at timestamptz,
|
| 22 |
+
payload jsonb NOT NULL
|
| 23 |
+
);
|
| 24 |
+
CREATE INDEX IF NOT EXISTS chat_sessions_owner_idx
|
| 25 |
+
ON chat_sessions (owner_lookup, updated_at DESC);
|
| 26 |
+
CREATE INDEX IF NOT EXISTS chat_sessions_expires_idx
|
| 27 |
+
ON chat_sessions (expires_at)
|
| 28 |
+
WHERE expires_at IS NOT NULL;
|
| 29 |
+
|
| 30 |
+
CREATE TABLE IF NOT EXISTS session_shares (
|
| 31 |
+
id text PRIMARY KEY,
|
| 32 |
+
token_lookup text NOT NULL UNIQUE,
|
| 33 |
+
owner_lookup text NOT NULL,
|
| 34 |
+
created_at timestamptz NOT NULL,
|
| 35 |
+
payload jsonb NOT NULL
|
| 36 |
+
);
|
| 37 |
+
CREATE INDEX IF NOT EXISTS session_shares_owner_idx
|
| 38 |
+
ON session_shares (owner_lookup, created_at DESC);
|
| 39 |
+
|
| 40 |
+
CREATE TABLE IF NOT EXISTS deleted_chats (
|
| 41 |
+
id text PRIMARY KEY,
|
| 42 |
+
owner_lookup text NOT NULL,
|
| 43 |
+
purge_at timestamptz,
|
| 44 |
+
deleted_at timestamptz NOT NULL,
|
| 45 |
+
payload jsonb NOT NULL
|
| 46 |
+
);
|
| 47 |
+
CREATE INDEX IF NOT EXISTS deleted_chats_owner_idx
|
| 48 |
+
ON deleted_chats (owner_lookup, deleted_at DESC);
|
| 49 |
+
CREATE INDEX IF NOT EXISTS deleted_chats_purge_idx
|
| 50 |
+
ON deleted_chats (purge_at)
|
| 51 |
+
WHERE purge_at IS NOT NULL;
|
| 52 |
+
|
| 53 |
+
CREATE TABLE IF NOT EXISTS memories (
|
| 54 |
+
id text PRIMARY KEY,
|
| 55 |
+
owner_lookup text NOT NULL,
|
| 56 |
+
created_at timestamptz NOT NULL,
|
| 57 |
+
updated_at timestamptz NOT NULL,
|
| 58 |
+
payload jsonb NOT NULL
|
| 59 |
+
);
|
| 60 |
+
CREATE INDEX IF NOT EXISTS memories_owner_idx
|
| 61 |
+
ON memories (owner_lookup, updated_at DESC);
|
| 62 |
+
|
| 63 |
+
CREATE TABLE IF NOT EXISTS media_entries (
|
| 64 |
+
id text PRIMARY KEY,
|
| 65 |
+
owner_lookup text NOT NULL,
|
| 66 |
+
parent_id text,
|
| 67 |
+
entry_type text NOT NULL,
|
| 68 |
+
updated_at timestamptz NOT NULL,
|
| 69 |
+
created_at timestamptz NOT NULL,
|
| 70 |
+
trashed_at timestamptz,
|
| 71 |
+
purge_at timestamptz,
|
| 72 |
+
expires_at timestamptz,
|
| 73 |
+
size_bytes bigint NOT NULL DEFAULT 0,
|
| 74 |
+
payload jsonb NOT NULL
|
| 75 |
+
);
|
| 76 |
+
CREATE INDEX IF NOT EXISTS media_entries_owner_idx
|
| 77 |
+
ON media_entries (owner_lookup, updated_at DESC);
|
| 78 |
+
CREATE INDEX IF NOT EXISTS media_entries_parent_idx
|
| 79 |
+
ON media_entries (owner_lookup, parent_id);
|
| 80 |
+
CREATE INDEX IF NOT EXISTS media_entries_purge_idx
|
| 81 |
+
ON media_entries (purge_at)
|
| 82 |
+
WHERE purge_at IS NOT NULL;
|
| 83 |
+
CREATE INDEX IF NOT EXISTS media_entries_expires_idx
|
| 84 |
+
ON media_entries (expires_at)
|
| 85 |
+
WHERE expires_at IS NOT NULL;
|
| 86 |
+
|
| 87 |
+
CREATE TABLE IF NOT EXISTS media_blobs (
|
| 88 |
+
entry_id text PRIMARY KEY REFERENCES media_entries(id) ON DELETE CASCADE,
|
| 89 |
+
updated_at timestamptz NOT NULL,
|
| 90 |
+
payload bytea NOT NULL
|
| 91 |
+
);
|
| 92 |
+
|
| 93 |
+
CREATE TABLE IF NOT EXISTS system_prompts (
|
| 94 |
+
owner_lookup text PRIMARY KEY,
|
| 95 |
+
updated_at timestamptz NOT NULL,
|
| 96 |
+
payload jsonb NOT NULL
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
CREATE TABLE IF NOT EXISTS feedback_tickets (
|
| 100 |
+
id text PRIMARY KEY,
|
| 101 |
+
status text NOT NULL,
|
| 102 |
+
submitted_at timestamptz NOT NULL,
|
| 103 |
+
payload jsonb NOT NULL
|
| 104 |
+
);
|
| 105 |
+
CREATE INDEX IF NOT EXISTS feedback_tickets_status_idx
|
| 106 |
+
ON feedback_tickets (status, submitted_at DESC);
|
| 107 |
+
|
| 108 |
+
CREATE TABLE IF NOT EXISTS guest_request_counters (
|
| 109 |
+
key_lookup text PRIMARY KEY,
|
| 110 |
+
expires_at timestamptz NOT NULL,
|
| 111 |
+
updated_at timestamptz NOT NULL,
|
| 112 |
+
payload jsonb NOT NULL
|
| 113 |
+
);
|
| 114 |
+
CREATE INDEX IF NOT EXISTS guest_request_counters_expires_idx
|
| 115 |
+
ON guest_request_counters (expires_at);
|
| 116 |
+
|
| 117 |
+
CREATE TABLE IF NOT EXISTS web_search_usage (
|
| 118 |
+
key_lookup text NOT NULL,
|
| 119 |
+
day_key text NOT NULL,
|
| 120 |
+
updated_at timestamptz NOT NULL,
|
| 121 |
+
payload jsonb NOT NULL,
|
| 122 |
+
PRIMARY KEY (key_lookup, day_key)
|
| 123 |
+
);
|
| 124 |
+
|
| 125 |
+
CREATE TABLE IF NOT EXISTS user_settings (
|
| 126 |
+
owner_lookup text PRIMARY KEY,
|
| 127 |
+
updated_at timestamptz NOT NULL,
|
| 128 |
+
payload jsonb NOT NULL
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
CREATE TABLE IF NOT EXISTS user_profiles (
|
| 132 |
+
owner_lookup text PRIMARY KEY,
|
| 133 |
+
username_lookup text UNIQUE,
|
| 134 |
+
updated_at timestamptz NOT NULL,
|
| 135 |
+
payload jsonb NOT NULL
|
| 136 |
+
);
|
| 137 |
+
|
| 138 |
+
CREATE TABLE IF NOT EXISTS device_sessions (
|
| 139 |
+
token_lookup text PRIMARY KEY,
|
| 140 |
+
user_lookup text NOT NULL,
|
| 141 |
+
active boolean NOT NULL,
|
| 142 |
+
created_at timestamptz NOT NULL,
|
| 143 |
+
last_seen_at timestamptz NOT NULL,
|
| 144 |
+
payload jsonb NOT NULL
|
| 145 |
+
);
|
| 146 |
+
CREATE INDEX IF NOT EXISTS device_sessions_user_idx
|
| 147 |
+
ON device_sessions (user_lookup, active, last_seen_at DESC);
|
| 148 |
+
`;
|
server/sessionStore.js
CHANGED
|
@@ -1,24 +1,91 @@
|
|
| 1 |
-
// sessionStore.js — access_token + Supabase RLS, no service role key needed.
|
| 2 |
-
// Device sessions live in memory only (restart clears them).
|
| 3 |
import { createClient } from '@supabase/supabase-js';
|
| 4 |
import crypto from 'crypto';
|
| 5 |
import { saveEncryptedJson, loadEncryptedJson } from './cryptoUtils.js';
|
| 6 |
import path from 'path';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
let _SUPABASE_URL, _SUPABASE_ANON_KEY;
|
| 9 |
export function initStoreConfig(url, key) { _SUPABASE_URL = url; _SUPABASE_ANON_KEY = key; }
|
| 10 |
|
| 11 |
-
const TEMP_TTL_MS
|
| 12 |
const TEMP_INACTIVITY = 12 * 60 * 60 * 1000;
|
| 13 |
-
const TEMP_MSG_LIMIT
|
| 14 |
|
| 15 |
-
const userCache
|
| 16 |
-
const tempStore
|
| 17 |
-
const devSessions = new Map();
|
|
|
|
|
|
|
| 18 |
|
| 19 |
const TEMP_STORE_FILE = '/data/temp_sessions.json';
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
async function loadTempStore() {
|
|
|
|
| 22 |
const data = await loadEncryptedJson(TEMP_STORE_FILE);
|
| 23 |
if (data) {
|
| 24 |
for (const [id, d] of Object.entries(data)) {
|
|
@@ -28,11 +95,13 @@ async function loadTempStore() {
|
|
| 28 |
created: d.created || Date.now(),
|
| 29 |
lastActive: d.lastActive || Date.now(),
|
| 30 |
});
|
|
|
|
| 31 |
}
|
| 32 |
}
|
| 33 |
}
|
| 34 |
|
| 35 |
async function saveTempStore() {
|
|
|
|
| 36 |
const data = {};
|
| 37 |
for (const [id, d] of tempStore) {
|
| 38 |
data[id] = {
|
|
@@ -45,17 +114,100 @@ async function saveTempStore() {
|
|
| 45 |
await saveEncryptedJson(TEMP_STORE_FILE, data);
|
| 46 |
}
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
const
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
function userClient(accessToken) {
|
| 61 |
return createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, {
|
|
@@ -64,120 +216,287 @@ function userClient(accessToken) {
|
|
| 64 |
});
|
| 65 |
}
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
export const sessionStore = {
|
| 68 |
-
// ── TEMP ────────────────────────────────────────────────────────────────
|
| 69 |
initTemp(t) {
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
|
| 81 |
-
d.sessions.set(s.id, s);
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
return s;
|
| 84 |
},
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
return s;
|
| 91 |
},
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
| 94 |
const restored = JSON.parse(JSON.stringify(session));
|
| 95 |
d.sessions.set(restored.id, restored);
|
| 96 |
d.lastActive = Date.now();
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
return restored;
|
| 99 |
},
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
let changed = false;
|
| 104 |
for (const temp of tempStore.values()) {
|
| 105 |
if (temp.sessions.delete(id)) changed = true;
|
| 106 |
}
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
return changed;
|
| 109 |
},
|
| 110 |
|
| 111 |
-
/**
|
| 112 |
-
* Copy temp sessions into the user's account on login.
|
| 113 |
-
* We intentionally do NOT delete from tempStore so the guest session
|
| 114 |
-
* remains usable if the user logs out again (and so the WS client's
|
| 115 |
-
* tempId still resolves while the tab is open).
|
| 116 |
-
* Sessions that already exist in the user account (same id) are skipped
|
| 117 |
-
* to avoid overwriting newer server data.
|
| 118 |
-
*/
|
| 119 |
async transferTempToUser(tempId, userId, accessToken) {
|
|
|
|
| 120 |
const d = tempStore.get(tempId);
|
| 121 |
if (!d || !d.sessions.size) return;
|
| 122 |
|
| 123 |
-
const uc = userClient(accessToken);
|
| 124 |
const user = this._ensureUser(userId);
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
for (const s of d.sessions.values()) {
|
| 127 |
-
// Skip sessions that are empty (never actually used)
|
| 128 |
if (!s.history || s.history.length === 0) continue;
|
| 129 |
-
|
| 130 |
-
// Skip if the user already has a session with the same id
|
| 131 |
if (user.sessions.has(s.id)) continue;
|
| 132 |
|
| 133 |
-
// Deep-clone so mutations to the user copy don't affect temp copy
|
| 134 |
const copy = JSON.parse(JSON.stringify(s));
|
| 135 |
user.sessions.set(copy.id, copy);
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
},
|
| 140 |
|
| 141 |
-
// ── USERS ────────────────────────────────────────────────────────────────
|
| 142 |
_ensureUser(uid) {
|
| 143 |
if (!userCache.has(uid)) userCache.set(uid, { sessions: new Map(), online: new Set() });
|
| 144 |
return userCache.get(uid);
|
| 145 |
},
|
|
|
|
| 146 |
async loadUserSessions(userId, accessToken) {
|
|
|
|
|
|
|
| 147 |
const uc = userClient(accessToken);
|
| 148 |
const { data, error } = await uc.from('web_sessions').select('*')
|
| 149 |
.eq('user_id', userId).order('updated_at', { ascending: false });
|
| 150 |
if (error) { console.error('loadUserSessions', error.message); return []; }
|
| 151 |
const user = this._ensureUser(userId);
|
| 152 |
-
for (const row of data || [])
|
| 153 |
-
user.sessions.set(row.id, {
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
return [...user.sessions.values()];
|
| 156 |
},
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
async createUserSession(userId, accessToken) {
|
|
|
|
| 160 |
const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
|
| 161 |
this._ensureUser(userId).sessions.set(s.id, s);
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
return s;
|
| 164 |
},
|
|
|
|
| 165 |
async restoreUserSession(userId, accessToken, session) {
|
|
|
|
| 166 |
const restored = JSON.parse(JSON.stringify(session));
|
| 167 |
this._ensureUser(userId).sessions.set(restored.id, restored);
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
return restored;
|
| 170 |
},
|
|
|
|
| 171 |
async updateUserSession(userId, accessToken, sessionId, patch) {
|
| 172 |
-
|
| 173 |
-
const
|
|
|
|
|
|
|
|
|
|
| 174 |
Object.assign(s, patch);
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
return s;
|
| 177 |
},
|
|
|
|
| 178 |
async deleteUserSession(userId, accessToken, id) {
|
| 179 |
try {
|
| 180 |
userCache.get(userId)?.sessions.delete(id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
const { error } = await userClient(accessToken)
|
| 182 |
.from('web_sessions')
|
| 183 |
.delete()
|
|
@@ -188,6 +507,7 @@ export const sessionStore = {
|
|
| 188 |
console.error('Unexpected deleteUserSession error:', ex);
|
| 189 |
}
|
| 190 |
},
|
|
|
|
| 191 |
async deleteAllUserSessions(userId, accessToken) {
|
| 192 |
const u = userCache.get(userId);
|
| 193 |
if (u) {
|
|
@@ -196,7 +516,16 @@ export const sessionStore = {
|
|
| 196 |
console.error('No user for ' + userId);
|
| 197 |
return null;
|
| 198 |
}
|
|
|
|
| 199 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
const { error } = await userClient(accessToken)
|
| 201 |
.from('web_sessions')
|
| 202 |
.delete()
|
|
@@ -206,57 +535,214 @@ export const sessionStore = {
|
|
| 206 |
console.error('Unexpected deleteAllUserSessions error:', ex);
|
| 207 |
}
|
| 208 |
},
|
|
|
|
| 209 |
async _persist(uc, userId, s) {
|
| 210 |
await uc.from('web_sessions').upsert({
|
| 211 |
-
id: s.id,
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
created_at: new Date(s.created).toISOString(),
|
| 214 |
});
|
| 215 |
},
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
async createShareToken(userId, accessToken, sessionId) {
|
| 220 |
-
const s = this.getUserSession(userId, sessionId);
|
|
|
|
| 221 |
const token = crypto.randomBytes(24).toString('base64url');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
const uc = userClient(accessToken);
|
| 223 |
const { error } = await uc.from('shared_sessions').insert({
|
| 224 |
-
token,
|
|
|
|
|
|
|
|
|
|
| 225 |
});
|
| 226 |
return error ? null : token;
|
| 227 |
},
|
|
|
|
| 228 |
async resolveShareToken(token) {
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
const uc = createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, { auth: { persistSession: false } });
|
| 231 |
const { data } = await uc.from('shared_sessions').select('*').eq('token', token).single();
|
| 232 |
return data || null;
|
| 233 |
},
|
|
|
|
| 234 |
async importSharedSession(userId, accessToken, token) {
|
| 235 |
-
const shared = await this.resolveShareToken(token);
|
|
|
|
| 236 |
const snap = shared.session_snapshot;
|
| 237 |
-
const newSession = {
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
this._ensureUser(userId).sessions.set(newSession.id, newSession);
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
return newSession;
|
| 243 |
},
|
| 244 |
};
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
export const deviceSessionStore = {
|
| 247 |
-
create(userId, ip, userAgent) {
|
| 248 |
const token = crypto.randomBytes(32).toString('hex');
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
return token;
|
| 252 |
},
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
},
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
},
|
| 262 |
};
|
|
|
|
|
|
|
|
|
|
| 1 |
import { createClient } from '@supabase/supabase-js';
|
| 2 |
import crypto from 'crypto';
|
| 3 |
import { saveEncryptedJson, loadEncryptedJson } from './cryptoUtils.js';
|
| 4 |
import path from 'path';
|
| 5 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 6 |
+
import {
|
| 7 |
+
decryptJsonPayload,
|
| 8 |
+
encryptJsonPayload,
|
| 9 |
+
makeLookupToken,
|
| 10 |
+
makeOwnerLookup,
|
| 11 |
+
pgQuery,
|
| 12 |
+
} from './postgres.js';
|
| 13 |
|
| 14 |
let _SUPABASE_URL, _SUPABASE_ANON_KEY;
|
| 15 |
export function initStoreConfig(url, key) { _SUPABASE_URL = url; _SUPABASE_ANON_KEY = key; }
|
| 16 |
|
| 17 |
+
const TEMP_TTL_MS = 24 * 60 * 60 * 1000;
|
| 18 |
const TEMP_INACTIVITY = 12 * 60 * 60 * 1000;
|
| 19 |
+
const TEMP_MSG_LIMIT = 10;
|
| 20 |
|
| 21 |
+
const userCache = new Map();
|
| 22 |
+
const tempStore = new Map();
|
| 23 |
+
const devSessions = new Map();
|
| 24 |
+
const loadedTempIds = new Set();
|
| 25 |
+
const loadedUserIds = new Set();
|
| 26 |
|
| 27 |
const TEMP_STORE_FILE = '/data/temp_sessions.json';
|
| 28 |
|
| 29 |
+
function nowIso() {
|
| 30 |
+
return new Date().toISOString();
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function tempOwner(tempId) {
|
| 34 |
+
return { type: 'guest', id: tempId };
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function userOwner(userId) {
|
| 38 |
+
return { type: 'user', id: userId };
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function guestStateLookup(tempId) {
|
| 42 |
+
return makeLookupToken('guest-state', tempId);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function guestStateAad(tempId) {
|
| 46 |
+
return `guest-state:${tempId}`;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function sessionAad(scopeType, sessionId) {
|
| 50 |
+
return `chat-session:${scopeType}:${sessionId}`;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function shareTokenLookup(token) {
|
| 54 |
+
return makeLookupToken('session-share-token', token);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function shareAad(recordId) {
|
| 58 |
+
return `session-share:${recordId}`;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function deviceTokenLookup(token) {
|
| 62 |
+
return makeLookupToken('device-session-token', token);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function deviceSessionAad(tokenLookup) {
|
| 66 |
+
return `device-session:${tokenLookup}`;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function guestExpiryRecord(tempData) {
|
| 70 |
+
const createdExpires = (tempData.created || Date.now()) + TEMP_TTL_MS;
|
| 71 |
+
const inactiveExpires = (tempData.lastActive || Date.now()) + TEMP_INACTIVITY;
|
| 72 |
+
return new Date(Math.min(createdExpires, inactiveExpires)).toISOString();
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function ensureTempRecord(tempId) {
|
| 76 |
+
if (!tempStore.has(tempId)) {
|
| 77 |
+
tempStore.set(tempId, {
|
| 78 |
+
sessions: new Map(),
|
| 79 |
+
msgCount: 0,
|
| 80 |
+
created: Date.now(),
|
| 81 |
+
lastActive: Date.now(),
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
return tempStore.get(tempId);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
async function loadTempStore() {
|
| 88 |
+
if (isPostgresStorageMode()) return;
|
| 89 |
const data = await loadEncryptedJson(TEMP_STORE_FILE);
|
| 90 |
if (data) {
|
| 91 |
for (const [id, d] of Object.entries(data)) {
|
|
|
|
| 95 |
created: d.created || Date.now(),
|
| 96 |
lastActive: d.lastActive || Date.now(),
|
| 97 |
});
|
| 98 |
+
loadedTempIds.add(id);
|
| 99 |
}
|
| 100 |
}
|
| 101 |
}
|
| 102 |
|
| 103 |
async function saveTempStore() {
|
| 104 |
+
if (isPostgresStorageMode()) return;
|
| 105 |
const data = {};
|
| 106 |
for (const [id, d] of tempStore) {
|
| 107 |
data[id] = {
|
|
|
|
| 114 |
await saveEncryptedJson(TEMP_STORE_FILE, data);
|
| 115 |
}
|
| 116 |
|
| 117 |
+
async function ensureSqlTempLoaded(tempId) {
|
| 118 |
+
if (!isPostgresStorageMode() || loadedTempIds.has(tempId)) return;
|
| 119 |
+
const owner = tempOwner(tempId);
|
| 120 |
+
const lookup = makeOwnerLookup(owner);
|
| 121 |
+
const [guestStateResult, sessionResult] = await Promise.all([
|
| 122 |
+
pgQuery('SELECT payload FROM guest_state WHERE owner_lookup = $1', [guestStateLookup(tempId)]),
|
| 123 |
+
pgQuery(
|
| 124 |
+
'SELECT id, payload FROM chat_sessions WHERE owner_lookup = $1 AND scope_type = $2 ORDER BY updated_at DESC',
|
| 125 |
+
[lookup, 'guest']
|
| 126 |
+
),
|
| 127 |
+
]);
|
| 128 |
|
| 129 |
+
const base = ensureTempRecord(tempId);
|
| 130 |
+
const guestState = guestStateResult.rows[0]
|
| 131 |
+
? decryptJsonPayload(guestStateResult.rows[0].payload, guestStateAad(tempId))
|
| 132 |
+
: null;
|
| 133 |
+
|
| 134 |
+
base.msgCount = Number(guestState?.msgCount) || base.msgCount || 0;
|
| 135 |
+
base.created = Number(guestState?.created) || base.created || Date.now();
|
| 136 |
+
base.lastActive = Number(guestState?.lastActive) || base.lastActive || Date.now();
|
| 137 |
+
base.sessions = new Map(
|
| 138 |
+
sessionResult.rows
|
| 139 |
+
.map((row) => decryptJsonPayload(row.payload, sessionAad('guest', row.id)))
|
| 140 |
+
.filter((session) => session?.id)
|
| 141 |
+
.map((session) => [session.id, session])
|
| 142 |
+
);
|
| 143 |
+
loadedTempIds.add(tempId);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
async function persistSqlTempState(tempId) {
|
| 147 |
+
const data = tempStore.get(tempId);
|
| 148 |
+
if (!data) return;
|
| 149 |
+
await pgQuery(
|
| 150 |
+
`INSERT INTO guest_state (owner_lookup, expires_at, updated_at, payload)
|
| 151 |
+
VALUES ($1, $2, $3, $4::jsonb)
|
| 152 |
+
ON CONFLICT (owner_lookup)
|
| 153 |
+
DO UPDATE SET expires_at = EXCLUDED.expires_at, updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 154 |
+
[
|
| 155 |
+
guestStateLookup(tempId),
|
| 156 |
+
guestExpiryRecord(data),
|
| 157 |
+
nowIso(),
|
| 158 |
+
JSON.stringify(encryptJsonPayload({
|
| 159 |
+
tempId,
|
| 160 |
+
msgCount: data.msgCount,
|
| 161 |
+
created: data.created,
|
| 162 |
+
lastActive: data.lastActive,
|
| 163 |
+
}, guestStateAad(tempId))),
|
| 164 |
+
]
|
| 165 |
+
);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
async function persistSqlSession(owner, scopeType, session, expiresAt = null) {
|
| 169 |
+
await pgQuery(
|
| 170 |
+
`INSERT INTO chat_sessions (id, scope_type, owner_lookup, created_at, updated_at, expires_at, payload)
|
| 171 |
+
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
| 172 |
+
ON CONFLICT (id)
|
| 173 |
+
DO UPDATE SET
|
| 174 |
+
scope_type = EXCLUDED.scope_type,
|
| 175 |
+
owner_lookup = EXCLUDED.owner_lookup,
|
| 176 |
+
created_at = EXCLUDED.created_at,
|
| 177 |
+
updated_at = EXCLUDED.updated_at,
|
| 178 |
+
expires_at = EXCLUDED.expires_at,
|
| 179 |
+
payload = EXCLUDED.payload`,
|
| 180 |
+
[
|
| 181 |
+
session.id,
|
| 182 |
+
scopeType,
|
| 183 |
+
makeOwnerLookup(owner),
|
| 184 |
+
new Date(session.created).toISOString(),
|
| 185 |
+
nowIso(),
|
| 186 |
+
expiresAt,
|
| 187 |
+
JSON.stringify(encryptJsonPayload(session, sessionAad(scopeType, session.id))),
|
| 188 |
+
]
|
| 189 |
+
);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
async function loadUserSessionsSql(userId) {
|
| 193 |
+
const user = sessionStore._ensureUser(userId);
|
| 194 |
+
const { rows } = await pgQuery(
|
| 195 |
+
'SELECT id, payload FROM chat_sessions WHERE owner_lookup = $1 AND scope_type = $2 ORDER BY updated_at DESC',
|
| 196 |
+
[makeOwnerLookup(userOwner(userId)), 'user']
|
| 197 |
+
);
|
| 198 |
+
user.sessions.clear();
|
| 199 |
+
for (const row of rows) {
|
| 200 |
+
const session = decryptJsonPayload(row.payload, sessionAad('user', row.id));
|
| 201 |
+
if (session?.id) user.sessions.set(session.id, session);
|
| 202 |
+
}
|
| 203 |
+
loadedUserIds.add(userId);
|
| 204 |
+
return [...user.sessions.values()];
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
async function ensureUserLoaded(userId) {
|
| 208 |
+
if (!isPostgresStorageMode() || loadedUserIds.has(userId)) return;
|
| 209 |
+
await loadUserSessionsSql(userId);
|
| 210 |
+
}
|
| 211 |
|
| 212 |
function userClient(accessToken) {
|
| 213 |
return createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, {
|
|
|
|
| 216 |
});
|
| 217 |
}
|
| 218 |
|
| 219 |
+
loadTempStore().catch((err) => console.error('Failed to load temp store:', err));
|
| 220 |
+
|
| 221 |
+
setInterval(async () => {
|
| 222 |
+
const now = Date.now();
|
| 223 |
+
const expiredTempIds = [];
|
| 224 |
+
|
| 225 |
+
for (const [id, d] of tempStore) {
|
| 226 |
+
if (now - d.created > TEMP_TTL_MS || now - d.lastActive > TEMP_INACTIVITY) {
|
| 227 |
+
tempStore.delete(id);
|
| 228 |
+
expiredTempIds.push(id);
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
if (isPostgresStorageMode()) {
|
| 233 |
+
try {
|
| 234 |
+
for (const tempId of expiredTempIds) {
|
| 235 |
+
loadedTempIds.delete(tempId);
|
| 236 |
+
await pgQuery('DELETE FROM guest_state WHERE owner_lookup = $1', [guestStateLookup(tempId)]);
|
| 237 |
+
}
|
| 238 |
+
await pgQuery(
|
| 239 |
+
`DELETE FROM chat_sessions
|
| 240 |
+
WHERE scope_type = 'guest' AND expires_at IS NOT NULL AND expires_at <= $1`,
|
| 241 |
+
[nowIso()]
|
| 242 |
+
);
|
| 243 |
+
await pgQuery(
|
| 244 |
+
`DELETE FROM guest_state
|
| 245 |
+
WHERE expires_at IS NOT NULL AND expires_at <= $1`,
|
| 246 |
+
[nowIso()]
|
| 247 |
+
);
|
| 248 |
+
} catch (err) {
|
| 249 |
+
console.error('Failed to prune SQL temp store:', err);
|
| 250 |
+
}
|
| 251 |
+
return;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
await saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 255 |
+
}, 30 * 60 * 1000);
|
| 256 |
+
|
| 257 |
export const sessionStore = {
|
|
|
|
| 258 |
initTemp(t) {
|
| 259 |
+
return ensureTempRecord(t);
|
| 260 |
+
},
|
| 261 |
+
|
| 262 |
+
async tempCanSend(t) {
|
| 263 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 264 |
+
const d = tempStore.get(t);
|
| 265 |
+
return d ? d.msgCount < TEMP_MSG_LIMIT : false;
|
| 266 |
+
},
|
| 267 |
+
|
| 268 |
+
async tempBump(t) {
|
| 269 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 270 |
+
const d = ensureTempRecord(t);
|
| 271 |
+
d.msgCount += 1;
|
| 272 |
+
d.lastActive = Date.now();
|
| 273 |
+
if (isPostgresStorageMode()) {
|
| 274 |
+
await persistSqlTempState(t);
|
| 275 |
+
} else {
|
| 276 |
+
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 277 |
+
}
|
| 278 |
+
},
|
| 279 |
+
|
| 280 |
+
async getTempSessions(t) {
|
| 281 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 282 |
+
return [...(tempStore.get(t)?.sessions.values() || [])];
|
| 283 |
+
},
|
| 284 |
+
|
| 285 |
+
async getTempSession(t, id) {
|
| 286 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 287 |
+
return tempStore.get(t)?.sessions.get(id) || null;
|
| 288 |
+
},
|
| 289 |
+
|
| 290 |
+
async createTempSession(t) {
|
| 291 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 292 |
+
const d = ensureTempRecord(t);
|
| 293 |
const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
|
| 294 |
+
d.sessions.set(s.id, s);
|
| 295 |
+
d.lastActive = Date.now();
|
| 296 |
+
|
| 297 |
+
if (isPostgresStorageMode()) {
|
| 298 |
+
await persistSqlSession(tempOwner(t), 'guest', s, guestExpiryRecord(d));
|
| 299 |
+
await persistSqlTempState(t);
|
| 300 |
+
} else {
|
| 301 |
+
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 302 |
+
}
|
| 303 |
return s;
|
| 304 |
},
|
| 305 |
+
|
| 306 |
+
async updateTempSession(t, id, patch) {
|
| 307 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 308 |
+
const d = tempStore.get(t);
|
| 309 |
+
if (!d) return null;
|
| 310 |
+
const s = d.sessions.get(id);
|
| 311 |
+
if (!s) return null;
|
| 312 |
+
Object.assign(s, patch);
|
| 313 |
+
d.lastActive = Date.now();
|
| 314 |
+
|
| 315 |
+
if (isPostgresStorageMode()) {
|
| 316 |
+
await persistSqlSession(tempOwner(t), 'guest', s, guestExpiryRecord(d));
|
| 317 |
+
await persistSqlTempState(t);
|
| 318 |
+
} else {
|
| 319 |
+
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 320 |
+
}
|
| 321 |
return s;
|
| 322 |
},
|
| 323 |
+
|
| 324 |
+
async restoreTempSession(t, session) {
|
| 325 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 326 |
+
const d = ensureTempRecord(t);
|
| 327 |
const restored = JSON.parse(JSON.stringify(session));
|
| 328 |
d.sessions.set(restored.id, restored);
|
| 329 |
d.lastActive = Date.now();
|
| 330 |
+
|
| 331 |
+
if (isPostgresStorageMode()) {
|
| 332 |
+
await persistSqlSession(tempOwner(t), 'guest', restored, guestExpiryRecord(d));
|
| 333 |
+
await persistSqlTempState(t);
|
| 334 |
+
} else {
|
| 335 |
+
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 336 |
+
}
|
| 337 |
return restored;
|
| 338 |
},
|
| 339 |
+
|
| 340 |
+
async deleteTempSession(t, id) {
|
| 341 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 342 |
+
tempStore.get(t)?.sessions.delete(id);
|
| 343 |
+
|
| 344 |
+
if (isPostgresStorageMode()) {
|
| 345 |
+
await pgQuery(
|
| 346 |
+
'DELETE FROM chat_sessions WHERE id = $1 AND scope_type = $2 AND owner_lookup = $3',
|
| 347 |
+
[id, 'guest', makeOwnerLookup(tempOwner(t))]
|
| 348 |
+
);
|
| 349 |
+
await persistSqlTempState(t);
|
| 350 |
+
} else {
|
| 351 |
+
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 352 |
+
}
|
| 353 |
+
},
|
| 354 |
+
|
| 355 |
+
async deleteTempAll(t) {
|
| 356 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(t);
|
| 357 |
+
tempStore.get(t)?.sessions.clear();
|
| 358 |
+
|
| 359 |
+
if (isPostgresStorageMode()) {
|
| 360 |
+
await pgQuery(
|
| 361 |
+
'DELETE FROM chat_sessions WHERE scope_type = $1 AND owner_lookup = $2',
|
| 362 |
+
['guest', makeOwnerLookup(tempOwner(t))]
|
| 363 |
+
);
|
| 364 |
+
await persistSqlTempState(t);
|
| 365 |
+
} else {
|
| 366 |
+
saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 367 |
+
}
|
| 368 |
+
},
|
| 369 |
+
|
| 370 |
+
async deleteTempSessionEverywhere(id) {
|
| 371 |
let changed = false;
|
| 372 |
for (const temp of tempStore.values()) {
|
| 373 |
if (temp.sessions.delete(id)) changed = true;
|
| 374 |
}
|
| 375 |
+
|
| 376 |
+
if (isPostgresStorageMode()) {
|
| 377 |
+
const result = await pgQuery(
|
| 378 |
+
'DELETE FROM chat_sessions WHERE id = $1 AND scope_type = $2',
|
| 379 |
+
[id, 'guest']
|
| 380 |
+
);
|
| 381 |
+
return changed || result.rowCount > 0;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
if (changed) saveTempStore().catch((err) => console.error('Failed to save temp store:', err));
|
| 385 |
return changed;
|
| 386 |
},
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
async transferTempToUser(tempId, userId, accessToken) {
|
| 389 |
+
if (isPostgresStorageMode()) await ensureSqlTempLoaded(tempId);
|
| 390 |
const d = tempStore.get(tempId);
|
| 391 |
if (!d || !d.sessions.size) return;
|
| 392 |
|
|
|
|
| 393 |
const user = this._ensureUser(userId);
|
| 394 |
+
if (isPostgresStorageMode()) await ensureUserLoaded(userId);
|
| 395 |
+
|
| 396 |
+
const uc = isPostgresStorageMode() ? null : userClient(accessToken);
|
| 397 |
|
| 398 |
for (const s of d.sessions.values()) {
|
|
|
|
| 399 |
if (!s.history || s.history.length === 0) continue;
|
|
|
|
|
|
|
| 400 |
if (user.sessions.has(s.id)) continue;
|
| 401 |
|
|
|
|
| 402 |
const copy = JSON.parse(JSON.stringify(s));
|
| 403 |
user.sessions.set(copy.id, copy);
|
| 404 |
+
|
| 405 |
+
if (isPostgresStorageMode()) {
|
| 406 |
+
await persistSqlSession(userOwner(userId), 'user', copy, null);
|
| 407 |
+
} else {
|
| 408 |
+
await this._persist(uc, userId, copy).catch((err) =>
|
| 409 |
+
console.error('transferTempToUser persist error:', err.message));
|
| 410 |
+
}
|
| 411 |
}
|
| 412 |
},
|
| 413 |
|
|
|
|
| 414 |
_ensureUser(uid) {
|
| 415 |
if (!userCache.has(uid)) userCache.set(uid, { sessions: new Map(), online: new Set() });
|
| 416 |
return userCache.get(uid);
|
| 417 |
},
|
| 418 |
+
|
| 419 |
async loadUserSessions(userId, accessToken) {
|
| 420 |
+
if (isPostgresStorageMode()) return loadUserSessionsSql(userId);
|
| 421 |
+
|
| 422 |
const uc = userClient(accessToken);
|
| 423 |
const { data, error } = await uc.from('web_sessions').select('*')
|
| 424 |
.eq('user_id', userId).order('updated_at', { ascending: false });
|
| 425 |
if (error) { console.error('loadUserSessions', error.message); return []; }
|
| 426 |
const user = this._ensureUser(userId);
|
| 427 |
+
for (const row of data || []) {
|
| 428 |
+
user.sessions.set(row.id, {
|
| 429 |
+
id: row.id,
|
| 430 |
+
name: row.name,
|
| 431 |
+
created: new Date(row.created_at).getTime(),
|
| 432 |
+
history: row.history || [],
|
| 433 |
+
model: row.model,
|
| 434 |
+
});
|
| 435 |
+
}
|
| 436 |
return [...user.sessions.values()];
|
| 437 |
},
|
| 438 |
+
|
| 439 |
+
getUserSessions(uid) {
|
| 440 |
+
return [...(userCache.get(uid)?.sessions.values() || [])];
|
| 441 |
+
},
|
| 442 |
+
|
| 443 |
+
getUserSession(uid, id) {
|
| 444 |
+
return userCache.get(uid)?.sessions.get(id) || null;
|
| 445 |
+
},
|
| 446 |
+
|
| 447 |
async createUserSession(userId, accessToken) {
|
| 448 |
+
if (isPostgresStorageMode()) await ensureUserLoaded(userId);
|
| 449 |
const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
|
| 450 |
this._ensureUser(userId).sessions.set(s.id, s);
|
| 451 |
+
|
| 452 |
+
if (isPostgresStorageMode()) {
|
| 453 |
+
await persistSqlSession(userOwner(userId), 'user', s, null);
|
| 454 |
+
} else {
|
| 455 |
+
await this._persist(userClient(accessToken), userId, s).catch(() => {});
|
| 456 |
+
}
|
| 457 |
return s;
|
| 458 |
},
|
| 459 |
+
|
| 460 |
async restoreUserSession(userId, accessToken, session) {
|
| 461 |
+
if (isPostgresStorageMode()) await ensureUserLoaded(userId);
|
| 462 |
const restored = JSON.parse(JSON.stringify(session));
|
| 463 |
this._ensureUser(userId).sessions.set(restored.id, restored);
|
| 464 |
+
|
| 465 |
+
if (isPostgresStorageMode()) {
|
| 466 |
+
await persistSqlSession(userOwner(userId), 'user', restored, null);
|
| 467 |
+
} else {
|
| 468 |
+
await this._persist(userClient(accessToken), userId, restored).catch(() => {});
|
| 469 |
+
}
|
| 470 |
return restored;
|
| 471 |
},
|
| 472 |
+
|
| 473 |
async updateUserSession(userId, accessToken, sessionId, patch) {
|
| 474 |
+
if (isPostgresStorageMode()) await ensureUserLoaded(userId);
|
| 475 |
+
const user = userCache.get(userId);
|
| 476 |
+
if (!user) { console.error('No user for ' + userId); return null; }
|
| 477 |
+
const s = user.sessions.get(sessionId);
|
| 478 |
+
if (!s) { console.error('No session found for ' + sessionId); return null; }
|
| 479 |
Object.assign(s, patch);
|
| 480 |
+
|
| 481 |
+
if (isPostgresStorageMode()) {
|
| 482 |
+
await persistSqlSession(userOwner(userId), 'user', s, null);
|
| 483 |
+
} else {
|
| 484 |
+
await this._persist(userClient(accessToken), userId, s).catch(() => {});
|
| 485 |
+
}
|
| 486 |
return s;
|
| 487 |
},
|
| 488 |
+
|
| 489 |
async deleteUserSession(userId, accessToken, id) {
|
| 490 |
try {
|
| 491 |
userCache.get(userId)?.sessions.delete(id);
|
| 492 |
+
if (isPostgresStorageMode()) {
|
| 493 |
+
await pgQuery(
|
| 494 |
+
'DELETE FROM chat_sessions WHERE id = $1 AND scope_type = $2 AND owner_lookup = $3',
|
| 495 |
+
[id, 'user', makeOwnerLookup(userOwner(userId))]
|
| 496 |
+
);
|
| 497 |
+
return;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
const { error } = await userClient(accessToken)
|
| 501 |
.from('web_sessions')
|
| 502 |
.delete()
|
|
|
|
| 507 |
console.error('Unexpected deleteUserSession error:', ex);
|
| 508 |
}
|
| 509 |
},
|
| 510 |
+
|
| 511 |
async deleteAllUserSessions(userId, accessToken) {
|
| 512 |
const u = userCache.get(userId);
|
| 513 |
if (u) {
|
|
|
|
| 516 |
console.error('No user for ' + userId);
|
| 517 |
return null;
|
| 518 |
}
|
| 519 |
+
|
| 520 |
try {
|
| 521 |
+
if (isPostgresStorageMode()) {
|
| 522 |
+
await pgQuery(
|
| 523 |
+
'DELETE FROM chat_sessions WHERE scope_type = $1 AND owner_lookup = $2',
|
| 524 |
+
['user', makeOwnerLookup(userOwner(userId))]
|
| 525 |
+
);
|
| 526 |
+
return;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
const { error } = await userClient(accessToken)
|
| 530 |
.from('web_sessions')
|
| 531 |
.delete()
|
|
|
|
| 535 |
console.error('Unexpected deleteAllUserSessions error:', ex);
|
| 536 |
}
|
| 537 |
},
|
| 538 |
+
|
| 539 |
async _persist(uc, userId, s) {
|
| 540 |
await uc.from('web_sessions').upsert({
|
| 541 |
+
id: s.id,
|
| 542 |
+
user_id: userId,
|
| 543 |
+
name: s.name,
|
| 544 |
+
history: s.history || [],
|
| 545 |
+
model: s.model || null,
|
| 546 |
+
updated_at: new Date().toISOString(),
|
| 547 |
created_at: new Date(s.created).toISOString(),
|
| 548 |
});
|
| 549 |
},
|
| 550 |
+
|
| 551 |
+
markOnline(uid, ws) {
|
| 552 |
+
this._ensureUser(uid).online.add(ws);
|
| 553 |
+
},
|
| 554 |
+
|
| 555 |
+
markOffline(uid, ws) {
|
| 556 |
+
userCache.get(uid)?.online.delete(ws);
|
| 557 |
+
},
|
| 558 |
+
|
| 559 |
async createShareToken(userId, accessToken, sessionId) {
|
| 560 |
+
const s = this.getUserSession(userId, sessionId);
|
| 561 |
+
if (!s) return null;
|
| 562 |
const token = crypto.randomBytes(24).toString('base64url');
|
| 563 |
+
|
| 564 |
+
if (isPostgresStorageMode()) {
|
| 565 |
+
const record = {
|
| 566 |
+
id: crypto.randomUUID(),
|
| 567 |
+
ownerId: userId,
|
| 568 |
+
sessionSnapshot: s,
|
| 569 |
+
createdAt: nowIso(),
|
| 570 |
+
};
|
| 571 |
+
await pgQuery(
|
| 572 |
+
`INSERT INTO session_shares (id, token_lookup, owner_lookup, created_at, payload)
|
| 573 |
+
VALUES ($1, $2, $3, $4, $5::jsonb)`,
|
| 574 |
+
[
|
| 575 |
+
record.id,
|
| 576 |
+
shareTokenLookup(token),
|
| 577 |
+
makeOwnerLookup(userOwner(userId)),
|
| 578 |
+
record.createdAt,
|
| 579 |
+
JSON.stringify(encryptJsonPayload(record, shareAad(record.id))),
|
| 580 |
+
]
|
| 581 |
+
);
|
| 582 |
+
return token;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
const uc = userClient(accessToken);
|
| 586 |
const { error } = await uc.from('shared_sessions').insert({
|
| 587 |
+
token,
|
| 588 |
+
owner_id: userId,
|
| 589 |
+
session_snapshot: s,
|
| 590 |
+
created_at: new Date().toISOString(),
|
| 591 |
});
|
| 592 |
return error ? null : token;
|
| 593 |
},
|
| 594 |
+
|
| 595 |
async resolveShareToken(token) {
|
| 596 |
+
if (isPostgresStorageMode()) {
|
| 597 |
+
const { rows } = await pgQuery(
|
| 598 |
+
'SELECT id, payload FROM session_shares WHERE token_lookup = $1',
|
| 599 |
+
[shareTokenLookup(token)]
|
| 600 |
+
);
|
| 601 |
+
const record = rows[0] ? decryptJsonPayload(rows[0].payload, shareAad(rows[0].id)) : null;
|
| 602 |
+
return record ? {
|
| 603 |
+
token,
|
| 604 |
+
owner_id: record.ownerId,
|
| 605 |
+
session_snapshot: record.sessionSnapshot,
|
| 606 |
+
created_at: record.createdAt,
|
| 607 |
+
} : null;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
const uc = createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, { auth: { persistSession: false } });
|
| 611 |
const { data } = await uc.from('shared_sessions').select('*').eq('token', token).single();
|
| 612 |
return data || null;
|
| 613 |
},
|
| 614 |
+
|
| 615 |
async importSharedSession(userId, accessToken, token) {
|
| 616 |
+
const shared = await this.resolveShareToken(token);
|
| 617 |
+
if (!shared) return null;
|
| 618 |
const snap = shared.session_snapshot;
|
| 619 |
+
const newSession = {
|
| 620 |
+
...snap,
|
| 621 |
+
id: crypto.randomUUID(),
|
| 622 |
+
name: `${snap.name} (shared)`,
|
| 623 |
+
created: Date.now(),
|
| 624 |
+
};
|
| 625 |
+
|
| 626 |
+
if (isPostgresStorageMode()) await ensureUserLoaded(userId);
|
| 627 |
this._ensureUser(userId).sessions.set(newSession.id, newSession);
|
| 628 |
+
|
| 629 |
+
if (isPostgresStorageMode()) {
|
| 630 |
+
await persistSqlSession(userOwner(userId), 'user', newSession, null);
|
| 631 |
+
} else {
|
| 632 |
+
const uc = userClient(accessToken);
|
| 633 |
+
await this._persist(uc, userId, newSession).catch(() => {});
|
| 634 |
+
}
|
| 635 |
return newSession;
|
| 636 |
},
|
| 637 |
};
|
| 638 |
|
| 639 |
+
async function upsertSqlDeviceSession(session) {
|
| 640 |
+
const lookup = deviceTokenLookup(session.token);
|
| 641 |
+
await pgQuery(
|
| 642 |
+
`INSERT INTO device_sessions (token_lookup, user_lookup, active, created_at, last_seen_at, payload)
|
| 643 |
+
VALUES ($1, $2, $3, $4, $5, $6::jsonb)
|
| 644 |
+
ON CONFLICT (token_lookup)
|
| 645 |
+
DO UPDATE SET
|
| 646 |
+
user_lookup = EXCLUDED.user_lookup,
|
| 647 |
+
active = EXCLUDED.active,
|
| 648 |
+
created_at = EXCLUDED.created_at,
|
| 649 |
+
last_seen_at = EXCLUDED.last_seen_at,
|
| 650 |
+
payload = EXCLUDED.payload`,
|
| 651 |
+
[
|
| 652 |
+
lookup,
|
| 653 |
+
makeOwnerLookup(userOwner(session.userId)),
|
| 654 |
+
!!session.active,
|
| 655 |
+
session.createdAt,
|
| 656 |
+
session.lastSeen,
|
| 657 |
+
JSON.stringify(encryptJsonPayload(session, deviceSessionAad(lookup))),
|
| 658 |
+
]
|
| 659 |
+
);
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
async function loadSqlDeviceSession(token) {
|
| 663 |
+
const lookup = deviceTokenLookup(token);
|
| 664 |
+
const { rows } = await pgQuery(
|
| 665 |
+
'SELECT payload FROM device_sessions WHERE token_lookup = $1',
|
| 666 |
+
[lookup]
|
| 667 |
+
);
|
| 668 |
+
return rows[0] ? decryptJsonPayload(rows[0].payload, deviceSessionAad(lookup)) : null;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
export const deviceSessionStore = {
|
| 672 |
+
async create(userId, ip, userAgent) {
|
| 673 |
const token = crypto.randomBytes(32).toString('hex');
|
| 674 |
+
const session = {
|
| 675 |
+
token,
|
| 676 |
+
userId,
|
| 677 |
+
ip,
|
| 678 |
+
userAgent,
|
| 679 |
+
createdAt: nowIso(),
|
| 680 |
+
lastSeen: nowIso(),
|
| 681 |
+
active: true,
|
| 682 |
+
};
|
| 683 |
+
devSessions.set(token, session);
|
| 684 |
+
if (isPostgresStorageMode()) {
|
| 685 |
+
await upsertSqlDeviceSession(session);
|
| 686 |
+
}
|
| 687 |
return token;
|
| 688 |
},
|
| 689 |
+
|
| 690 |
+
async getForUser(uid) {
|
| 691 |
+
if (isPostgresStorageMode()) {
|
| 692 |
+
const { rows } = await pgQuery(
|
| 693 |
+
'SELECT token_lookup, payload FROM device_sessions WHERE user_lookup = $1 AND active = TRUE ORDER BY last_seen_at DESC',
|
| 694 |
+
[makeOwnerLookup(userOwner(uid))]
|
| 695 |
+
);
|
| 696 |
+
const sessions = rows
|
| 697 |
+
.map((row) => decryptJsonPayload(row.payload, deviceSessionAad(row.token_lookup)))
|
| 698 |
+
.filter((session) => session?.userId === uid && session.active);
|
| 699 |
+
for (const session of sessions) devSessions.set(session.token, session);
|
| 700 |
+
return sessions;
|
| 701 |
+
}
|
| 702 |
+
return [...devSessions.values()].filter((s) => s.userId === uid && s.active);
|
| 703 |
+
},
|
| 704 |
+
|
| 705 |
+
async revoke(token) {
|
| 706 |
+
let session = devSessions.get(token) || null;
|
| 707 |
+
if (isPostgresStorageMode() && !session) {
|
| 708 |
+
session = await loadSqlDeviceSession(token);
|
| 709 |
+
}
|
| 710 |
+
if (!session) return null;
|
| 711 |
+
session.active = false;
|
| 712 |
+
devSessions.set(token, session);
|
| 713 |
+
if (isPostgresStorageMode()) {
|
| 714 |
+
await upsertSqlDeviceSession(session);
|
| 715 |
+
}
|
| 716 |
+
return session;
|
| 717 |
+
},
|
| 718 |
+
|
| 719 |
+
async revokeAllExcept(uid, except) {
|
| 720 |
+
if (isPostgresStorageMode()) {
|
| 721 |
+
const sessions = await this.getForUser(uid);
|
| 722 |
+
for (const session of sessions) {
|
| 723 |
+
if (session.token === except) continue;
|
| 724 |
+
session.active = false;
|
| 725 |
+
devSessions.set(session.token, session);
|
| 726 |
+
await upsertSqlDeviceSession(session);
|
| 727 |
+
}
|
| 728 |
+
return;
|
| 729 |
+
}
|
| 730 |
+
for (const [t, s] of devSessions) {
|
| 731 |
+
if (s.userId === uid && t !== except) s.active = false;
|
| 732 |
+
}
|
| 733 |
},
|
| 734 |
+
|
| 735 |
+
async validate(token) {
|
| 736 |
+
let session = devSessions.get(token) || null;
|
| 737 |
+
if (isPostgresStorageMode() && !session) {
|
| 738 |
+
session = await loadSqlDeviceSession(token);
|
| 739 |
+
}
|
| 740 |
+
if (!session || !session.active) return null;
|
| 741 |
+
session.lastSeen = nowIso();
|
| 742 |
+
devSessions.set(token, session);
|
| 743 |
+
if (isPostgresStorageMode()) {
|
| 744 |
+
await upsertSqlDeviceSession(session);
|
| 745 |
+
}
|
| 746 |
+
return session;
|
| 747 |
},
|
| 748 |
};
|
server/systemPromptStore.js
CHANGED
|
@@ -2,6 +2,13 @@ import fs from 'fs/promises';
|
|
| 2 |
import path from 'path';
|
| 3 |
import { fileURLToPath } from 'url';
|
| 4 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 7 |
const SYSTEM_PROMPT_FILE = path.resolve(__dirname, '..', 'system prompt.md');
|
|
@@ -92,8 +99,16 @@ function normalizePrompt(markdown) {
|
|
| 92 |
.slice(0, MAX_PROMPT_LENGTH);
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
async function ensureLoaded() {
|
| 96 |
-
if (state.loaded) return;
|
| 97 |
const stored = await loadEncryptedJson(INDEX_FILE, 'system-prompts');
|
| 98 |
state.prompts = stored?.prompts || {};
|
| 99 |
state.loaded = true;
|
|
@@ -120,6 +135,14 @@ function sanitizeRecord(record) {
|
|
| 120 |
};
|
| 121 |
}
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
export const systemPromptStore = {
|
| 124 |
async getDefaultPrompt() {
|
| 125 |
return loadDefaultPrompt();
|
|
@@ -127,6 +150,8 @@ export const systemPromptStore = {
|
|
| 127 |
|
| 128 |
async getUserPrompt(userId) {
|
| 129 |
if (!userId) return null;
|
|
|
|
|
|
|
| 130 |
await ensureLoaded();
|
| 131 |
return sanitizeRecord(state.prompts[userId]);
|
| 132 |
},
|
|
@@ -155,6 +180,27 @@ export const systemPromptStore = {
|
|
| 155 |
if (!userId) throw new Error('Missing user id');
|
| 156 |
const normalized = normalizePrompt(markdown);
|
| 157 |
if (!normalized) throw new Error('System prompt cannot be empty');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
await ensureLoaded();
|
| 159 |
state.prompts[userId] = {
|
| 160 |
markdown: normalized,
|
|
@@ -166,6 +212,12 @@ export const systemPromptStore = {
|
|
| 166 |
|
| 167 |
async resetUserPrompt(userId) {
|
| 168 |
if (!userId) throw new Error('Missing user id');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
await ensureLoaded();
|
| 170 |
delete state.prompts[userId];
|
| 171 |
await saveIndex();
|
|
|
|
| 2 |
import path from 'path';
|
| 3 |
import { fileURLToPath } from 'url';
|
| 4 |
import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js';
|
| 5 |
+
import { isPostgresStorageMode } from './dataPaths.js';
|
| 6 |
+
import {
|
| 7 |
+
decryptJsonPayload,
|
| 8 |
+
encryptJsonPayload,
|
| 9 |
+
makeLookupToken,
|
| 10 |
+
pgQuery,
|
| 11 |
+
} from './postgres.js';
|
| 12 |
|
| 13 |
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 14 |
const SYSTEM_PROMPT_FILE = path.resolve(__dirname, '..', 'system prompt.md');
|
|
|
|
| 99 |
.slice(0, MAX_PROMPT_LENGTH);
|
| 100 |
}
|
| 101 |
|
| 102 |
+
function promptLookup(userId) {
|
| 103 |
+
return makeLookupToken('system-prompt', userId);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function promptAad(userId) {
|
| 107 |
+
return `system-prompt:${userId}`;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
async function ensureLoaded() {
|
| 111 |
+
if (state.loaded || isPostgresStorageMode()) return;
|
| 112 |
const stored = await loadEncryptedJson(INDEX_FILE, 'system-prompts');
|
| 113 |
state.prompts = stored?.prompts || {};
|
| 114 |
state.loaded = true;
|
|
|
|
| 135 |
};
|
| 136 |
}
|
| 137 |
|
| 138 |
+
async function getSqlPrompt(userId) {
|
| 139 |
+
const { rows } = await pgQuery(
|
| 140 |
+
'SELECT payload FROM system_prompts WHERE owner_lookup = $1',
|
| 141 |
+
[promptLookup(userId)]
|
| 142 |
+
);
|
| 143 |
+
return rows[0] ? sanitizeRecord(decryptJsonPayload(rows[0].payload, promptAad(userId))) : null;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
export const systemPromptStore = {
|
| 147 |
async getDefaultPrompt() {
|
| 148 |
return loadDefaultPrompt();
|
|
|
|
| 150 |
|
| 151 |
async getUserPrompt(userId) {
|
| 152 |
if (!userId) return null;
|
| 153 |
+
if (isPostgresStorageMode()) return getSqlPrompt(userId);
|
| 154 |
+
|
| 155 |
await ensureLoaded();
|
| 156 |
return sanitizeRecord(state.prompts[userId]);
|
| 157 |
},
|
|
|
|
| 180 |
if (!userId) throw new Error('Missing user id');
|
| 181 |
const normalized = normalizePrompt(markdown);
|
| 182 |
if (!normalized) throw new Error('System prompt cannot be empty');
|
| 183 |
+
|
| 184 |
+
if (isPostgresStorageMode()) {
|
| 185 |
+
const record = {
|
| 186 |
+
userId,
|
| 187 |
+
markdown: normalized,
|
| 188 |
+
updatedAt: new Date().toISOString(),
|
| 189 |
+
};
|
| 190 |
+
await pgQuery(
|
| 191 |
+
`INSERT INTO system_prompts (owner_lookup, updated_at, payload)
|
| 192 |
+
VALUES ($1, $2, $3::jsonb)
|
| 193 |
+
ON CONFLICT (owner_lookup)
|
| 194 |
+
DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 195 |
+
[
|
| 196 |
+
promptLookup(userId),
|
| 197 |
+
record.updatedAt,
|
| 198 |
+
JSON.stringify(encryptJsonPayload(record, promptAad(userId))),
|
| 199 |
+
]
|
| 200 |
+
);
|
| 201 |
+
return this.getPersonalization(userId);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
await ensureLoaded();
|
| 205 |
state.prompts[userId] = {
|
| 206 |
markdown: normalized,
|
|
|
|
| 212 |
|
| 213 |
async resetUserPrompt(userId) {
|
| 214 |
if (!userId) throw new Error('Missing user id');
|
| 215 |
+
|
| 216 |
+
if (isPostgresStorageMode()) {
|
| 217 |
+
await pgQuery('DELETE FROM system_prompts WHERE owner_lookup = $1', [promptLookup(userId)]);
|
| 218 |
+
return this.getPersonalization(userId);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
await ensureLoaded();
|
| 222 |
delete state.prompts[userId];
|
| 223 |
await saveIndex();
|
server/versionStore.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs/promises';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { DATA_ROOT, isPostgresStorageMode } from './dataPaths.js';
|
| 4 |
+
import {
|
| 5 |
+
decryptJsonPayload,
|
| 6 |
+
encryptJsonPayload,
|
| 7 |
+
makeLookupToken,
|
| 8 |
+
pgQuery,
|
| 9 |
+
} from './postgres.js';
|
| 10 |
+
|
| 11 |
+
const VERSION_FILE = path.join(DATA_ROOT, 'version.json');
|
| 12 |
+
|
| 13 |
+
function versionLookup(publicUrl) {
|
| 14 |
+
return makeLookupToken('app-version', publicUrl);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
function versionAad(publicUrl) {
|
| 18 |
+
return `app-version:${publicUrl}`;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function loadStoredSHA(publicUrl) {
|
| 22 |
+
if (isPostgresStorageMode()) {
|
| 23 |
+
const { rows } = await pgQuery(
|
| 24 |
+
'SELECT payload FROM app_versions WHERE public_url_lookup = $1',
|
| 25 |
+
[versionLookup(publicUrl)]
|
| 26 |
+
);
|
| 27 |
+
const payload = rows[0]
|
| 28 |
+
? decryptJsonPayload(rows[0].payload, versionAad(publicUrl))
|
| 29 |
+
: null;
|
| 30 |
+
return payload?.sha || null;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
try {
|
| 34 |
+
await fs.mkdir(DATA_ROOT, { recursive: true });
|
| 35 |
+
const raw = await fs.readFile(VERSION_FILE, 'utf8');
|
| 36 |
+
const data = JSON.parse(raw);
|
| 37 |
+
const obj = Array.isArray(data) ? data.find((item) => item[publicUrl]) : null;
|
| 38 |
+
return obj ? obj[publicUrl] : null;
|
| 39 |
+
} catch {
|
| 40 |
+
return null;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export async function saveStoredSHA(publicUrl, sha) {
|
| 45 |
+
if (isPostgresStorageMode()) {
|
| 46 |
+
await pgQuery(
|
| 47 |
+
`INSERT INTO app_versions (public_url_lookup, updated_at, payload)
|
| 48 |
+
VALUES ($1, $2, $3::jsonb)
|
| 49 |
+
ON CONFLICT (public_url_lookup)
|
| 50 |
+
DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 51 |
+
[
|
| 52 |
+
versionLookup(publicUrl),
|
| 53 |
+
new Date().toISOString(),
|
| 54 |
+
JSON.stringify(encryptJsonPayload({ publicUrl, sha }, versionAad(publicUrl))),
|
| 55 |
+
]
|
| 56 |
+
);
|
| 57 |
+
return;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
await fs.mkdir(DATA_ROOT, { recursive: true });
|
| 61 |
+
|
| 62 |
+
let data = [];
|
| 63 |
+
try {
|
| 64 |
+
const raw = await fs.readFile(VERSION_FILE, 'utf8');
|
| 65 |
+
const parsed = JSON.parse(raw);
|
| 66 |
+
data = Array.isArray(parsed) ? parsed : [];
|
| 67 |
+
} catch {
|
| 68 |
+
data = [];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
let found = false;
|
| 72 |
+
for (const entry of data) {
|
| 73 |
+
if (entry[publicUrl]) {
|
| 74 |
+
entry[publicUrl] = sha;
|
| 75 |
+
found = true;
|
| 76 |
+
break;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if (!found) {
|
| 81 |
+
data.push({ [publicUrl]: sha });
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
await fs.writeFile(VERSION_FILE, JSON.stringify(data, null, 2), 'utf8');
|
| 85 |
+
}
|
server/webSearchUsageStore.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
| 1 |
import fs from 'fs/promises';
|
| 2 |
import path from 'path';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
const
|
| 5 |
-
const STORE_FILE = path.join(DATA_DIR, 'web-search-usage.json');
|
| 6 |
const APP_TIMEZONE = process.env.APP_TIMEZONE || 'America/New_York';
|
| 7 |
|
| 8 |
let state = { days: {} };
|
|
@@ -18,6 +25,14 @@ function todayKey() {
|
|
| 18 |
}).format(new Date());
|
| 19 |
}
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
function pruneDays() {
|
| 22 |
const keepKey = todayKey();
|
| 23 |
state.days = Object.fromEntries(
|
|
@@ -26,10 +41,10 @@ function pruneDays() {
|
|
| 26 |
}
|
| 27 |
|
| 28 |
async function ensureLoaded() {
|
| 29 |
-
if (loaded) return;
|
| 30 |
loaded = true;
|
| 31 |
try {
|
| 32 |
-
await fs.mkdir(
|
| 33 |
const raw = await fs.readFile(STORE_FILE, 'utf8');
|
| 34 |
const parsed = JSON.parse(raw);
|
| 35 |
if (parsed && typeof parsed === 'object') state = parsed;
|
|
@@ -40,7 +55,7 @@ async function ensureLoaded() {
|
|
| 40 |
function saveState() {
|
| 41 |
saveChain = saveChain.then(async () => {
|
| 42 |
pruneDays();
|
| 43 |
-
await fs.mkdir(
|
| 44 |
await fs.writeFile(STORE_FILE, JSON.stringify(state, null, 2), 'utf8');
|
| 45 |
}).catch((err) => {
|
| 46 |
console.error('Failed to persist web search usage:', err);
|
|
@@ -61,7 +76,80 @@ function getCounterRecord(key) {
|
|
| 61 |
};
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
export async function getWebSearchUsage(key, limit = 15) {
|
|
|
|
|
|
|
| 65 |
await ensureLoaded();
|
| 66 |
const record = getCounterRecord(key);
|
| 67 |
return {
|
|
@@ -74,6 +162,8 @@ export async function getWebSearchUsage(key, limit = 15) {
|
|
| 74 |
}
|
| 75 |
|
| 76 |
export async function consumeWebSearchUsage(key, limit = 15) {
|
|
|
|
|
|
|
| 77 |
await ensureLoaded();
|
| 78 |
const record = getCounterRecord(key);
|
| 79 |
if (record.used >= limit) {
|
|
|
|
| 1 |
import fs from 'fs/promises';
|
| 2 |
import path from 'path';
|
| 3 |
+
import { DATA_ROOT, isPostgresStorageMode } from './dataPaths.js';
|
| 4 |
+
import {
|
| 5 |
+
decryptJsonPayload,
|
| 6 |
+
encryptJsonPayload,
|
| 7 |
+
makeLookupToken,
|
| 8 |
+
pgQuery,
|
| 9 |
+
withPgTransaction,
|
| 10 |
+
} from './postgres.js';
|
| 11 |
|
| 12 |
+
const STORE_FILE = path.join(DATA_ROOT, 'web-search-usage.json');
|
|
|
|
| 13 |
const APP_TIMEZONE = process.env.APP_TIMEZONE || 'America/New_York';
|
| 14 |
|
| 15 |
let state = { days: {} };
|
|
|
|
| 25 |
}).format(new Date());
|
| 26 |
}
|
| 27 |
|
| 28 |
+
function usageLookup(key) {
|
| 29 |
+
return makeLookupToken('web-search-usage', key);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function usageAad(key, day) {
|
| 33 |
+
return `web-search-usage:${key}:${day}`;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
function pruneDays() {
|
| 37 |
const keepKey = todayKey();
|
| 38 |
state.days = Object.fromEntries(
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
async function ensureLoaded() {
|
| 44 |
+
if (loaded || isPostgresStorageMode()) return;
|
| 45 |
loaded = true;
|
| 46 |
try {
|
| 47 |
+
await fs.mkdir(DATA_ROOT, { recursive: true });
|
| 48 |
const raw = await fs.readFile(STORE_FILE, 'utf8');
|
| 49 |
const parsed = JSON.parse(raw);
|
| 50 |
if (parsed && typeof parsed === 'object') state = parsed;
|
|
|
|
| 55 |
function saveState() {
|
| 56 |
saveChain = saveChain.then(async () => {
|
| 57 |
pruneDays();
|
| 58 |
+
await fs.mkdir(DATA_ROOT, { recursive: true });
|
| 59 |
await fs.writeFile(STORE_FILE, JSON.stringify(state, null, 2), 'utf8');
|
| 60 |
}).catch((err) => {
|
| 61 |
console.error('Failed to persist web search usage:', err);
|
|
|
|
| 76 |
};
|
| 77 |
}
|
| 78 |
|
| 79 |
+
async function pruneSqlUsage(day, client = null) {
|
| 80 |
+
const runner = client || { query: pgQuery };
|
| 81 |
+
await runner.query('DELETE FROM web_search_usage WHERE day_key <> $1', [day]);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
async function getSqlUsage(key, limit = 15) {
|
| 85 |
+
const day = todayKey();
|
| 86 |
+
await pruneSqlUsage(day);
|
| 87 |
+
const lookup = usageLookup(key);
|
| 88 |
+
const { rows } = await pgQuery(
|
| 89 |
+
'SELECT payload FROM web_search_usage WHERE key_lookup = $1 AND day_key = $2',
|
| 90 |
+
[lookup, day]
|
| 91 |
+
);
|
| 92 |
+
const payload = rows[0]
|
| 93 |
+
? decryptJsonPayload(rows[0].payload, usageAad(key, day))
|
| 94 |
+
: null;
|
| 95 |
+
const used = Math.max(0, Number(payload?.used) || 0);
|
| 96 |
+
return {
|
| 97 |
+
limit,
|
| 98 |
+
used,
|
| 99 |
+
remaining: Math.max(0, limit - used),
|
| 100 |
+
window: day,
|
| 101 |
+
period: 'daily',
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
async function consumeSqlUsage(key, limit = 15) {
|
| 106 |
+
return withPgTransaction(async (client) => {
|
| 107 |
+
const day = todayKey();
|
| 108 |
+
await pruneSqlUsage(day, client);
|
| 109 |
+
const lookup = usageLookup(key);
|
| 110 |
+
const { rows } = await client.query(
|
| 111 |
+
'SELECT payload FROM web_search_usage WHERE key_lookup = $1 AND day_key = $2 FOR UPDATE',
|
| 112 |
+
[lookup, day]
|
| 113 |
+
);
|
| 114 |
+
const current = rows[0]
|
| 115 |
+
? decryptJsonPayload(rows[0].payload, usageAad(key, day))
|
| 116 |
+
: { used: 0 };
|
| 117 |
+
const used = Math.max(0, Number(current?.used) || 0);
|
| 118 |
+
|
| 119 |
+
if (used >= limit) {
|
| 120 |
+
return {
|
| 121 |
+
allowed: false,
|
| 122 |
+
limit,
|
| 123 |
+
used,
|
| 124 |
+
remaining: 0,
|
| 125 |
+
window: day,
|
| 126 |
+
period: 'daily',
|
| 127 |
+
};
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
const next = { used: used + 1 };
|
| 131 |
+
await client.query(
|
| 132 |
+
`INSERT INTO web_search_usage (key_lookup, day_key, updated_at, payload)
|
| 133 |
+
VALUES ($1, $2, $3, $4::jsonb)
|
| 134 |
+
ON CONFLICT (key_lookup, day_key)
|
| 135 |
+
DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`,
|
| 136 |
+
[lookup, day, new Date().toISOString(), JSON.stringify(encryptJsonPayload(next, usageAad(key, day)))]
|
| 137 |
+
);
|
| 138 |
+
|
| 139 |
+
return {
|
| 140 |
+
allowed: true,
|
| 141 |
+
limit,
|
| 142 |
+
used: next.used,
|
| 143 |
+
remaining: Math.max(0, limit - next.used),
|
| 144 |
+
window: day,
|
| 145 |
+
period: 'daily',
|
| 146 |
+
};
|
| 147 |
+
});
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
export async function getWebSearchUsage(key, limit = 15) {
|
| 151 |
+
if (isPostgresStorageMode()) return getSqlUsage(key, limit);
|
| 152 |
+
|
| 153 |
await ensureLoaded();
|
| 154 |
const record = getCounterRecord(key);
|
| 155 |
return {
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
export async function consumeWebSearchUsage(key, limit = 15) {
|
| 165 |
+
if (isPostgresStorageMode()) return consumeSqlUsage(key, limit);
|
| 166 |
+
|
| 167 |
await ensureLoaded();
|
| 168 |
const record = getCounterRecord(key);
|
| 169 |
if (record.used >= limit) {
|
server/wsHandler.js
CHANGED
|
@@ -97,7 +97,7 @@ export async function handleWsMessage(ws, msg, wsClients) {
|
|
| 97 |
'auth:logout',
|
| 98 |
]);
|
| 99 |
if (client.userId && client.deviceToken && !bypassDeviceValidation.has(msg.type)) {
|
| 100 |
-
const activeDeviceSession = deviceSessionStore.validate(client.deviceToken);
|
| 101 |
if (!activeDeviceSession) {
|
| 102 |
const priorUserId = client.userId;
|
| 103 |
Object.assign(client, { userId: null, authenticated: false, accessToken: null, deviceToken: null });
|
|
@@ -141,7 +141,7 @@ const handlers = {
|
|
| 141 |
let nextDeviceToken = null;
|
| 142 |
let reusedDeviceSession = false;
|
| 143 |
if (requestedDeviceToken) {
|
| 144 |
-
const existingDevice = deviceSessionStore.validate(requestedDeviceToken);
|
| 145 |
if (existingDevice?.userId === user.id) {
|
| 146 |
existingDevice.ip = client.ip;
|
| 147 |
existingDevice.userAgent = client.userAgent;
|
|
@@ -150,10 +150,10 @@ const handlers = {
|
|
| 150 |
}
|
| 151 |
}
|
| 152 |
if (!nextDeviceToken) {
|
| 153 |
-
nextDeviceToken = deviceSessionStore.create(user.id, client.ip, client.userAgent);
|
| 154 |
}
|
| 155 |
if (client.deviceToken && client.deviceToken !== nextDeviceToken) {
|
| 156 |
-
deviceSessionStore.revoke(client.deviceToken);
|
| 157 |
}
|
| 158 |
client.userId = user.id; client.accessToken = accessToken; client.authenticated = true;
|
| 159 |
client.deviceToken = nextDeviceToken;
|
|
@@ -180,25 +180,26 @@ const handlers = {
|
|
| 180 |
}
|
| 181 |
},
|
| 182 |
|
| 183 |
-
'auth:logout': (ws, msg, client) => {
|
| 184 |
const priorUserId = client.userId;
|
| 185 |
-
if (client.deviceToken) deviceSessionStore.revoke(client.deviceToken);
|
| 186 |
Object.assign(client, { userId: null, authenticated: false, accessToken: null, deviceToken: null });
|
| 187 |
if (priorUserId) sessionStore.markOffline(priorUserId, ws);
|
| 188 |
safeSend(ws, { type: 'auth:loggedOut' });
|
| 189 |
},
|
| 190 |
|
| 191 |
-
'auth:guest': (ws, msg, client) => {
|
| 192 |
const t = msg.tempId || client.tempId;
|
| 193 |
client.tempId = t;
|
| 194 |
sessionStore.initTemp(t);
|
| 195 |
-
|
|
|
|
| 196 |
},
|
| 197 |
|
| 198 |
-
'sessions:list': (ws, msg, client) => {
|
| 199 |
const list = client.userId
|
| 200 |
? sessionStore.getUserSessions(client.userId)
|
| 201 |
-
: sessionStore.getTempSessions(client.tempId);
|
| 202 |
list.sort((a, b) => b.created - a.created);
|
| 203 |
safeSend(ws, { type: 'sessions:list', sessions: list.map(ser) });
|
| 204 |
},
|
|
@@ -206,7 +207,7 @@ const handlers = {
|
|
| 206 |
'sessions:create': async (ws, msg, client) => {
|
| 207 |
const s = client.userId
|
| 208 |
? await sessionStore.createUserSession(client.userId, client.accessToken)
|
| 209 |
-
: sessionStore.createTempSession(client.tempId);
|
| 210 |
safeSend(ws, { type: 'sessions:created', session: ser(s) });
|
| 211 |
},
|
| 212 |
|
|
@@ -214,15 +215,15 @@ const handlers = {
|
|
| 214 |
const owner = getClientOwner(client);
|
| 215 |
const session = client.userId
|
| 216 |
? sessionStore.getUserSession(client.userId, msg.sessionId)
|
| 217 |
-
: sessionStore.getTempSession(client.tempId, msg.sessionId);
|
| 218 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 219 |
|
| 220 |
await chatTrashStore.add(owner, JSON.parse(JSON.stringify(session)));
|
| 221 |
if (client.userId) {
|
| 222 |
await sessionStore.deleteUserSession(client.userId, client.accessToken, msg.sessionId);
|
| 223 |
-
sessionStore.deleteTempSessionEverywhere(msg.sessionId);
|
| 224 |
} else {
|
| 225 |
-
sessionStore.deleteTempSession(client.tempId, msg.sessionId);
|
| 226 |
}
|
| 227 |
safeSend(ws, { type: 'sessions:deleted', sessionId: msg.sessionId });
|
| 228 |
safeSend(ws, { type: 'trash:chats:changed' });
|
|
@@ -232,13 +233,13 @@ const handlers = {
|
|
| 232 |
const owner = getClientOwner(client);
|
| 233 |
const sessions = client.userId
|
| 234 |
? sessionStore.getUserSessions(client.userId)
|
| 235 |
-
: sessionStore.getTempSessions(client.tempId);
|
| 236 |
for (const session of sessions) {
|
| 237 |
await chatTrashStore.add(owner, JSON.parse(JSON.stringify(session)));
|
| 238 |
-
if (client.userId) sessionStore.deleteTempSessionEverywhere(session.id);
|
| 239 |
}
|
| 240 |
if (client.userId) await sessionStore.deleteAllUserSessions(client.userId, client.accessToken);
|
| 241 |
-
else sessionStore.deleteTempAll(client.tempId);
|
| 242 |
safeSend(ws, { type: 'sessions:deletedAll' });
|
| 243 |
safeSend(ws, { type: 'trash:chats:changed' });
|
| 244 |
},
|
|
@@ -247,14 +248,14 @@ const handlers = {
|
|
| 247 |
const name = (msg.name || '').trim(); if (!name) return;
|
| 248 |
if (client.userId)
|
| 249 |
await sessionStore.updateUserSession(client.userId, client.accessToken, msg.sessionId, { name });
|
| 250 |
-
else sessionStore.updateTempSession(client.tempId, msg.sessionId, { name });
|
| 251 |
safeSend(ws, { type: 'sessions:renamed', sessionId: msg.sessionId, name });
|
| 252 |
},
|
| 253 |
|
| 254 |
-
'sessions:get': (ws, msg, client) => {
|
| 255 |
const s = client.userId
|
| 256 |
? sessionStore.getUserSession(client.userId, msg.sessionId)
|
| 257 |
-
: sessionStore.getTempSession(client.tempId, msg.sessionId);
|
| 258 |
if (!s) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 259 |
safeSend(ws, { type: 'sessions:data', session: ser(s) });
|
| 260 |
},
|
|
@@ -304,12 +305,12 @@ const handlers = {
|
|
| 304 |
if (!client.userId) {
|
| 305 |
const allowed = await consumeGuestRequest(client.ip || 'unknown');
|
| 306 |
if (!allowed) return safeSend(ws, { type: 'guest:rateLimit', message: 'Guest request limit exceeded' });
|
| 307 |
-
if (!sessionStore.tempCanSend(client.tempId)) return safeSend(ws, { type: 'chat:limitReached' });
|
| 308 |
-
sessionStore.tempBump(client.tempId);
|
| 309 |
}
|
| 310 |
const session = client.userId
|
| 311 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 312 |
-
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 313 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 314 |
|
| 315 |
abortActiveStream(ws);
|
|
@@ -431,7 +432,7 @@ const handlers = {
|
|
| 431 |
|
| 432 |
if (client.userId)
|
| 433 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory, name: newName });
|
| 434 |
-
else sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 435 |
|
| 436 |
safeSend(ws, { type: aborted ? 'chat:aborted' : 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRootMessage) });
|
| 437 |
},
|
|
@@ -450,7 +451,7 @@ const handlers = {
|
|
| 450 |
const { sessionId, messageIndex, newContent } = msg;
|
| 451 |
const session = client.userId
|
| 452 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 453 |
-
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 454 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 455 |
|
| 456 |
const rootMessage = session.history?.[0];
|
|
@@ -492,7 +493,7 @@ const handlers = {
|
|
| 492 |
if (client.userId) {
|
| 493 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
|
| 494 |
} else {
|
| 495 |
-
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
|
| 496 |
}
|
| 497 |
|
| 498 |
// Send back the updated message with its ID and the full flat history
|
|
@@ -511,7 +512,7 @@ const handlers = {
|
|
| 511 |
const { sessionId, messageIndex, versionIdx } = msg;
|
| 512 |
const session = client.userId
|
| 513 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 514 |
-
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 515 |
if (!session) return;
|
| 516 |
|
| 517 |
const rootMessage = session.history?.[0];
|
|
@@ -535,7 +536,7 @@ const handlers = {
|
|
| 535 |
if (client.userId) {
|
| 536 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
|
| 537 |
} else {
|
| 538 |
-
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
|
| 539 |
}
|
| 540 |
|
| 541 |
// Send back with messageId for clarity
|
|
@@ -547,7 +548,7 @@ const handlers = {
|
|
| 547 |
const action = msg.action === 'continue' ? 'continue' : 'regenerate';
|
| 548 |
const session = client.userId
|
| 549 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 550 |
-
: sessionStore.getTempSession(client.tempId, sessionId);
|
| 551 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 552 |
|
| 553 |
const rootMessage = session.history?.[0];
|
|
@@ -685,7 +686,7 @@ const handlers = {
|
|
| 685 |
if (client.userId) {
|
| 686 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory, name: newName });
|
| 687 |
} else {
|
| 688 |
-
sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 689 |
}
|
| 690 |
|
| 691 |
safeSend(ws, { type: 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRoot) });
|
|
@@ -787,12 +788,19 @@ const handlers = {
|
|
| 787 |
},
|
| 788 |
'account:getUsage': async (ws, msg, c) => { safeSend(ws, { type: 'account:usage', usage: await buildUsagePayload(c, msg.clientId || '') }); },
|
| 789 |
'account:getTierConfig': async (ws) => { safeSend(ws, { type: 'account:tierConfig', config: await getTierConfig() }); },
|
| 790 |
-
'account:getSessions': (ws, msg, c)
|
| 791 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
if (!c.userId || !msg.token) return;
|
| 793 |
-
const revoked = deviceSessionStore.revoke(msg.token);
|
| 794 |
if (revoked) {
|
| 795 |
-
const activeSessions = deviceSessionStore.getForUser(c.userId);
|
| 796 |
for (const [ows, oc] of wsClients) {
|
| 797 |
if (oc.userId !== c.userId) continue;
|
| 798 |
if (oc.deviceToken === msg.token) {
|
|
@@ -810,10 +818,10 @@ const handlers = {
|
|
| 810 |
}
|
| 811 |
safeSend(ws, { type: 'account:sessionRevoked', token: msg.token });
|
| 812 |
},
|
| 813 |
-
'account:revokeAllOthers': (ws, msg, c, wsClients) => {
|
| 814 |
if (!c.userId) return;
|
| 815 |
-
deviceSessionStore.revokeAllExcept(c.userId, c.deviceToken);
|
| 816 |
-
const activeSessions = deviceSessionStore.getForUser(c.userId);
|
| 817 |
for (const [ows, oc] of wsClients) {
|
| 818 |
if (oc.userId !== c.userId) continue;
|
| 819 |
if (oc.deviceToken && oc.deviceToken !== c.deviceToken) {
|
|
@@ -845,7 +853,7 @@ async function restoreDeletedSession(client, snapshot) {
|
|
| 845 |
const restored = JSON.parse(JSON.stringify(snapshot));
|
| 846 |
const existing = client.userId
|
| 847 |
? sessionStore.getUserSession(client.userId, restored.id)
|
| 848 |
-
: sessionStore.getTempSession(client.tempId, restored.id);
|
| 849 |
if (existing) restored.id = crypto.randomUUID();
|
| 850 |
restored.created = restored.created || Date.now();
|
| 851 |
if (client.userId) {
|
|
|
|
| 97 |
'auth:logout',
|
| 98 |
]);
|
| 99 |
if (client.userId && client.deviceToken && !bypassDeviceValidation.has(msg.type)) {
|
| 100 |
+
const activeDeviceSession = await deviceSessionStore.validate(client.deviceToken);
|
| 101 |
if (!activeDeviceSession) {
|
| 102 |
const priorUserId = client.userId;
|
| 103 |
Object.assign(client, { userId: null, authenticated: false, accessToken: null, deviceToken: null });
|
|
|
|
| 141 |
let nextDeviceToken = null;
|
| 142 |
let reusedDeviceSession = false;
|
| 143 |
if (requestedDeviceToken) {
|
| 144 |
+
const existingDevice = await deviceSessionStore.validate(requestedDeviceToken);
|
| 145 |
if (existingDevice?.userId === user.id) {
|
| 146 |
existingDevice.ip = client.ip;
|
| 147 |
existingDevice.userAgent = client.userAgent;
|
|
|
|
| 150 |
}
|
| 151 |
}
|
| 152 |
if (!nextDeviceToken) {
|
| 153 |
+
nextDeviceToken = await deviceSessionStore.create(user.id, client.ip, client.userAgent);
|
| 154 |
}
|
| 155 |
if (client.deviceToken && client.deviceToken !== nextDeviceToken) {
|
| 156 |
+
await deviceSessionStore.revoke(client.deviceToken);
|
| 157 |
}
|
| 158 |
client.userId = user.id; client.accessToken = accessToken; client.authenticated = true;
|
| 159 |
client.deviceToken = nextDeviceToken;
|
|
|
|
| 180 |
}
|
| 181 |
},
|
| 182 |
|
| 183 |
+
'auth:logout': async (ws, msg, client) => {
|
| 184 |
const priorUserId = client.userId;
|
| 185 |
+
if (client.deviceToken) await deviceSessionStore.revoke(client.deviceToken);
|
| 186 |
Object.assign(client, { userId: null, authenticated: false, accessToken: null, deviceToken: null });
|
| 187 |
if (priorUserId) sessionStore.markOffline(priorUserId, ws);
|
| 188 |
safeSend(ws, { type: 'auth:loggedOut' });
|
| 189 |
},
|
| 190 |
|
| 191 |
+
'auth:guest': async (ws, msg, client) => {
|
| 192 |
const t = msg.tempId || client.tempId;
|
| 193 |
client.tempId = t;
|
| 194 |
sessionStore.initTemp(t);
|
| 195 |
+
const sessions = await sessionStore.getTempSessions(t);
|
| 196 |
+
safeSend(ws, { type: 'auth:guestOk', tempId: t, sessions: sessions.map(ser) });
|
| 197 |
},
|
| 198 |
|
| 199 |
+
'sessions:list': async (ws, msg, client) => {
|
| 200 |
const list = client.userId
|
| 201 |
? sessionStore.getUserSessions(client.userId)
|
| 202 |
+
: await sessionStore.getTempSessions(client.tempId);
|
| 203 |
list.sort((a, b) => b.created - a.created);
|
| 204 |
safeSend(ws, { type: 'sessions:list', sessions: list.map(ser) });
|
| 205 |
},
|
|
|
|
| 207 |
'sessions:create': async (ws, msg, client) => {
|
| 208 |
const s = client.userId
|
| 209 |
? await sessionStore.createUserSession(client.userId, client.accessToken)
|
| 210 |
+
: await sessionStore.createTempSession(client.tempId);
|
| 211 |
safeSend(ws, { type: 'sessions:created', session: ser(s) });
|
| 212 |
},
|
| 213 |
|
|
|
|
| 215 |
const owner = getClientOwner(client);
|
| 216 |
const session = client.userId
|
| 217 |
? sessionStore.getUserSession(client.userId, msg.sessionId)
|
| 218 |
+
: await sessionStore.getTempSession(client.tempId, msg.sessionId);
|
| 219 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 220 |
|
| 221 |
await chatTrashStore.add(owner, JSON.parse(JSON.stringify(session)));
|
| 222 |
if (client.userId) {
|
| 223 |
await sessionStore.deleteUserSession(client.userId, client.accessToken, msg.sessionId);
|
| 224 |
+
await sessionStore.deleteTempSessionEverywhere(msg.sessionId);
|
| 225 |
} else {
|
| 226 |
+
await sessionStore.deleteTempSession(client.tempId, msg.sessionId);
|
| 227 |
}
|
| 228 |
safeSend(ws, { type: 'sessions:deleted', sessionId: msg.sessionId });
|
| 229 |
safeSend(ws, { type: 'trash:chats:changed' });
|
|
|
|
| 233 |
const owner = getClientOwner(client);
|
| 234 |
const sessions = client.userId
|
| 235 |
? sessionStore.getUserSessions(client.userId)
|
| 236 |
+
: await sessionStore.getTempSessions(client.tempId);
|
| 237 |
for (const session of sessions) {
|
| 238 |
await chatTrashStore.add(owner, JSON.parse(JSON.stringify(session)));
|
| 239 |
+
if (client.userId) await sessionStore.deleteTempSessionEverywhere(session.id);
|
| 240 |
}
|
| 241 |
if (client.userId) await sessionStore.deleteAllUserSessions(client.userId, client.accessToken);
|
| 242 |
+
else await sessionStore.deleteTempAll(client.tempId);
|
| 243 |
safeSend(ws, { type: 'sessions:deletedAll' });
|
| 244 |
safeSend(ws, { type: 'trash:chats:changed' });
|
| 245 |
},
|
|
|
|
| 248 |
const name = (msg.name || '').trim(); if (!name) return;
|
| 249 |
if (client.userId)
|
| 250 |
await sessionStore.updateUserSession(client.userId, client.accessToken, msg.sessionId, { name });
|
| 251 |
+
else await sessionStore.updateTempSession(client.tempId, msg.sessionId, { name });
|
| 252 |
safeSend(ws, { type: 'sessions:renamed', sessionId: msg.sessionId, name });
|
| 253 |
},
|
| 254 |
|
| 255 |
+
'sessions:get': async (ws, msg, client) => {
|
| 256 |
const s = client.userId
|
| 257 |
? sessionStore.getUserSession(client.userId, msg.sessionId)
|
| 258 |
+
: await sessionStore.getTempSession(client.tempId, msg.sessionId);
|
| 259 |
if (!s) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 260 |
safeSend(ws, { type: 'sessions:data', session: ser(s) });
|
| 261 |
},
|
|
|
|
| 305 |
if (!client.userId) {
|
| 306 |
const allowed = await consumeGuestRequest(client.ip || 'unknown');
|
| 307 |
if (!allowed) return safeSend(ws, { type: 'guest:rateLimit', message: 'Guest request limit exceeded' });
|
| 308 |
+
if (!await sessionStore.tempCanSend(client.tempId)) return safeSend(ws, { type: 'chat:limitReached' });
|
| 309 |
+
await sessionStore.tempBump(client.tempId);
|
| 310 |
}
|
| 311 |
const session = client.userId
|
| 312 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 313 |
+
: await sessionStore.getTempSession(client.tempId, sessionId);
|
| 314 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 315 |
|
| 316 |
abortActiveStream(ws);
|
|
|
|
| 432 |
|
| 433 |
if (client.userId)
|
| 434 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory, name: newName });
|
| 435 |
+
else await sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 436 |
|
| 437 |
safeSend(ws, { type: aborted ? 'chat:aborted' : 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRootMessage) });
|
| 438 |
},
|
|
|
|
| 451 |
const { sessionId, messageIndex, newContent } = msg;
|
| 452 |
const session = client.userId
|
| 453 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 454 |
+
: await sessionStore.getTempSession(client.tempId, sessionId);
|
| 455 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 456 |
|
| 457 |
const rootMessage = session.history?.[0];
|
|
|
|
| 493 |
if (client.userId) {
|
| 494 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
|
| 495 |
} else {
|
| 496 |
+
await sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
|
| 497 |
}
|
| 498 |
|
| 499 |
// Send back the updated message with its ID and the full flat history
|
|
|
|
| 512 |
const { sessionId, messageIndex, versionIdx } = msg;
|
| 513 |
const session = client.userId
|
| 514 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 515 |
+
: await sessionStore.getTempSession(client.tempId, sessionId);
|
| 516 |
if (!session) return;
|
| 517 |
|
| 518 |
const rootMessage = session.history?.[0];
|
|
|
|
| 536 |
if (client.userId) {
|
| 537 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
|
| 538 |
} else {
|
| 539 |
+
await sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
|
| 540 |
}
|
| 541 |
|
| 542 |
// Send back with messageId for clarity
|
|
|
|
| 548 |
const action = msg.action === 'continue' ? 'continue' : 'regenerate';
|
| 549 |
const session = client.userId
|
| 550 |
? sessionStore.getUserSession(client.userId, sessionId)
|
| 551 |
+
: await sessionStore.getTempSession(client.tempId, sessionId);
|
| 552 |
if (!session) return safeSend(ws, { type: 'error', message: 'Session not found' });
|
| 553 |
|
| 554 |
const rootMessage = session.history?.[0];
|
|
|
|
| 686 |
if (client.userId) {
|
| 687 |
await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory, name: newName });
|
| 688 |
} else {
|
| 689 |
+
await sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory, name: newName });
|
| 690 |
}
|
| 691 |
|
| 692 |
safeSend(ws, { type: 'chat:done', sessionId, name: newName, history: extractFlatHistory(newRoot) });
|
|
|
|
| 788 |
},
|
| 789 |
'account:getUsage': async (ws, msg, c) => { safeSend(ws, { type: 'account:usage', usage: await buildUsagePayload(c, msg.clientId || '') }); },
|
| 790 |
'account:getTierConfig': async (ws) => { safeSend(ws, { type: 'account:tierConfig', config: await getTierConfig() }); },
|
| 791 |
+
'account:getSessions': async (ws, msg, c) => {
|
| 792 |
+
if (!c.userId) return;
|
| 793 |
+
safeSend(ws, {
|
| 794 |
+
type: 'account:deviceSessions',
|
| 795 |
+
sessions: await deviceSessionStore.getForUser(c.userId),
|
| 796 |
+
currentToken: c.deviceToken,
|
| 797 |
+
});
|
| 798 |
+
},
|
| 799 |
+
'account:revokeSession': async (ws, msg, c, wsClients) => {
|
| 800 |
if (!c.userId || !msg.token) return;
|
| 801 |
+
const revoked = await deviceSessionStore.revoke(msg.token);
|
| 802 |
if (revoked) {
|
| 803 |
+
const activeSessions = await deviceSessionStore.getForUser(c.userId);
|
| 804 |
for (const [ows, oc] of wsClients) {
|
| 805 |
if (oc.userId !== c.userId) continue;
|
| 806 |
if (oc.deviceToken === msg.token) {
|
|
|
|
| 818 |
}
|
| 819 |
safeSend(ws, { type: 'account:sessionRevoked', token: msg.token });
|
| 820 |
},
|
| 821 |
+
'account:revokeAllOthers': async (ws, msg, c, wsClients) => {
|
| 822 |
if (!c.userId) return;
|
| 823 |
+
await deviceSessionStore.revokeAllExcept(c.userId, c.deviceToken);
|
| 824 |
+
const activeSessions = await deviceSessionStore.getForUser(c.userId);
|
| 825 |
for (const [ows, oc] of wsClients) {
|
| 826 |
if (oc.userId !== c.userId) continue;
|
| 827 |
if (oc.deviceToken && oc.deviceToken !== c.deviceToken) {
|
|
|
|
| 853 |
const restored = JSON.parse(JSON.stringify(snapshot));
|
| 854 |
const existing = client.userId
|
| 855 |
? sessionStore.getUserSession(client.userId, restored.id)
|
| 856 |
+
: await sessionStore.getTempSession(client.tempId, restored.id);
|
| 857 |
if (existing) restored.id = crypto.randomUUID();
|
| 858 |
restored.created = restored.created || Date.now();
|
| 859 |
if (client.userId) {
|