incognitolm commited on
Commit
bff1056
·
1 Parent(s): c97ad08

Migration to PostgreSQL

Browse files
.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
- await loadGuestCounters().catch(err => console.error('Failed to load guest request counters:', err));
 
 
 
 
 
 
 
 
 
 
 
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 fs.rm(blobPathFor(id), { force: true }).catch(() => {});
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
- await saveIndex();
 
 
 
 
 
 
 
 
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 readEncryptedFile(blobPathFor(id), buildAad(entry)),
373
  };
374
  },
375
 
@@ -397,8 +524,15 @@ export const mediaStore = {
397
  entry.size = nextSize;
398
  entry.updatedAt = nowIso();
399
 
400
- await writeEncryptedFile(blobPathFor(entry.id), Buffer.from(buffer), buildAad(entry));
401
- await saveIndex();
 
 
 
 
 
 
 
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 (updated.length) await saveIndex();
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 (trashed.length) await saveIndex();
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 (restored.length) await saveIndex();
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 (updated.length) await saveIndex();
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 = 24 * 60 * 60 * 1000;
12
  const TEMP_INACTIVITY = 12 * 60 * 60 * 1000;
13
- const TEMP_MSG_LIMIT = 10;
14
 
15
- const userCache = new Map(); // userId -> { sessions: Map, online: Set }
16
- const tempStore = new Map(); // tempId -> TempData
17
- const devSessions = new Map(); // token -> DeviceSession
 
 
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
- // Load temp store on init
49
- loadTempStore().catch(err => console.error('Failed to load temp store:', err));
 
 
 
 
 
 
 
 
 
50
 
51
- setInterval(async () => {
52
- const now = Date.now();
53
- for (const [id, d] of tempStore)
54
- if (now - d.created > TEMP_TTL_MS || now - d.lastActive > TEMP_INACTIVITY)
55
- tempStore.delete(id);
56
- // Save after cleanup
57
- await saveTempStore().catch(err => console.error('Failed to save temp store:', err));
58
- }, 30 * 60 * 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if (!tempStore.has(t))
71
- tempStore.set(t, { sessions: new Map(), msgCount: 0, created: Date.now(), lastActive: Date.now() });
72
- return tempStore.get(t);
73
- },
74
- tempCanSend(t) { const d = tempStore.get(t); return d ? d.msgCount < TEMP_MSG_LIMIT : false; },
75
- tempBump(t) { const d = tempStore.get(t); if (d) { d.msgCount++; d.lastActive = Date.now(); } },
76
- getTempSessions(t) { return [...(tempStore.get(t)?.sessions.values() || [])]; },
77
- getTempSession(t, id) { return tempStore.get(t)?.sessions.get(id) || null; },
78
- createTempSession(t) {
79
- const d = this.initTemp(t);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] };
81
- d.sessions.set(s.id, s); d.lastActive = Date.now();
82
- saveTempStore().catch(err => console.error('Failed to save temp store:', err));
 
 
 
 
 
 
 
83
  return s;
84
  },
85
- updateTempSession(t, id, patch) {
86
- const d = tempStore.get(t); if (!d) return null;
87
- const s = d.sessions.get(id); if (!s) return null;
88
- Object.assign(s, patch); d.lastActive = Date.now();
89
- saveTempStore().catch(err => console.error('Failed to save temp store:', err));
 
 
 
 
 
 
 
 
 
 
 
90
  return s;
91
  },
92
- restoreTempSession(t, session) {
93
- const d = this.initTemp(t);
 
 
94
  const restored = JSON.parse(JSON.stringify(session));
95
  d.sessions.set(restored.id, restored);
96
  d.lastActive = Date.now();
97
- saveTempStore().catch(err => console.error('Failed to save temp store:', err));
 
 
 
 
 
 
98
  return restored;
99
  },
100
- deleteTempSession(t, id) { tempStore.get(t)?.sessions.delete(id); saveTempStore().catch(err => console.error('Failed to save temp store:', err)); },
101
- deleteTempAll(t) { tempStore.get(t)?.sessions.clear(); saveTempStore().catch(err => console.error('Failed to save temp store:', err)); },
102
- deleteTempSessionEverywhere(id) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  let changed = false;
104
  for (const temp of tempStore.values()) {
105
  if (temp.sessions.delete(id)) changed = true;
106
  }
107
- if (changed) saveTempStore().catch(err => console.error('Failed to save temp store:', err));
 
 
 
 
 
 
 
 
 
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
- await this._persist(uc, userId, copy).catch(err =>
137
- console.error('transferTempToUser persist error:', err.message));
 
 
 
 
 
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, { id: row.id, name: row.name,
154
- created: new Date(row.created_at).getTime(), history: row.history || [], model: row.model });
 
 
 
 
 
 
155
  return [...user.sessions.values()];
156
  },
157
- getUserSessions(uid) { return [...(userCache.get(uid)?.sessions.values() || [])]; },
158
- getUserSession(uid, id) { return userCache.get(uid)?.sessions.get(id) || null; },
 
 
 
 
 
 
 
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
- await this._persist(userClient(accessToken), userId, s).catch(() => {});
 
 
 
 
 
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
- await this._persist(userClient(accessToken), userId, restored).catch(() => {});
 
 
 
 
 
169
  return restored;
170
  },
 
171
  async updateUserSession(userId, accessToken, sessionId, patch) {
172
- const user = userCache.get(userId); if (!user) { console.error("No user for " + userId); return null; }
173
- const s = user.sessions.get(sessionId); if (!s) { console.error ("No session found for " + sessionId); return null; }
 
 
 
174
  Object.assign(s, patch);
175
- await this._persist(userClient(accessToken), userId, s).catch(() => {});
 
 
 
 
 
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, user_id: userId, name: s.name, history: s.history || [],
212
- model: s.model || null, updated_at: new Date().toISOString(),
 
 
 
 
213
  created_at: new Date(s.created).toISOString(),
214
  });
215
  },
216
- markOnline(uid, ws) { this._ensureUser(uid).online.add(ws); },
217
- markOffline(uid, ws) { userCache.get(uid)?.online.delete(ws); },
218
- // ── SHARE ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
219
  async createShareToken(userId, accessToken, sessionId) {
220
- const s = this.getUserSession(userId, sessionId); if (!s) return null;
 
221
  const token = crypto.randomBytes(24).toString('base64url');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  const uc = userClient(accessToken);
223
  const { error } = await uc.from('shared_sessions').insert({
224
- token, owner_id: userId, session_snapshot: s, created_at: new Date().toISOString(),
 
 
 
225
  });
226
  return error ? null : token;
227
  },
 
228
  async resolveShareToken(token) {
229
- // shared_sessions SELECT is public via RLS
 
 
 
 
 
 
 
 
 
 
 
 
 
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); if (!shared) return null;
 
236
  const snap = shared.session_snapshot;
237
- const newSession = { ...snap, id: crypto.randomUUID(),
238
- name: `${snap.name} (shared)`, created: Date.now() };
239
- const uc = userClient(accessToken);
 
 
 
 
 
240
  this._ensureUser(userId).sessions.set(newSession.id, newSession);
241
- await this._persist(uc, userId, newSession).catch(() => {});
 
 
 
 
 
 
242
  return newSession;
243
  },
244
  };
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  export const deviceSessionStore = {
247
- create(userId, ip, userAgent) {
248
  const token = crypto.randomBytes(32).toString('hex');
249
- devSessions.set(token, { token, userId, ip, userAgent,
250
- createdAt: new Date().toISOString(), lastSeen: new Date().toISOString(), active: true });
 
 
 
 
 
 
 
 
 
 
 
251
  return token;
252
  },
253
- getForUser(uid) { return [...devSessions.values()].filter(s => s.userId === uid && s.active); },
254
- revoke(token) { const s = devSessions.get(token); if (s) { s.active = false; return s; } return null; },
255
- revokeAllExcept(uid, except) {
256
- for (const [t, s] of devSessions) if (s.userId === uid && t !== except) s.active = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  },
258
- validate(token) {
259
- const s = devSessions.get(token); if (!s || !s.active) return null;
260
- s.lastSeen = new Date().toISOString(); return s;
 
 
 
 
 
 
 
 
 
 
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 DATA_DIR = '/data';
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(DATA_DIR, { recursive: true });
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(DATA_DIR, { recursive: true });
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
- safeSend(ws, { type: 'auth:guestOk', tempId: t, sessions: sessionStore.getTempSessions(t).map(ser) });
 
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) => { if (!c.userId) return; safeSend(ws, { type: 'account:deviceSessions', sessions: deviceSessionStore.getForUser(c.userId), currentToken: c.deviceToken }); },
791
- 'account:revokeSession': (ws, msg, c, wsClients) => {
 
 
 
 
 
 
 
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) {