most-box 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +222 -182
  3. package/electron/main.js +67 -0
  4. package/out/404/index.html +2 -2
  5. package/out/404.html +2 -2
  6. package/out/__next.__PAGE__.txt +6 -4
  7. package/out/__next._full.txt +20 -17
  8. package/out/__next._head.txt +3 -3
  9. package/out/__next._index.txt +6 -5
  10. package/out/__next._tree.txt +4 -2
  11. package/out/_next/static/chunks/003jnm.v5tzw5.js +1 -0
  12. package/out/_next/static/chunks/00re8v.gbcywn.js +1 -0
  13. package/out/_next/static/chunks/00s106sbq8t9v.js +1 -0
  14. package/out/_next/static/chunks/012hi627qrdnn.js +1 -0
  15. package/out/_next/static/chunks/0174xh3wfsjm1.js +2 -0
  16. package/out/_next/static/chunks/01xlw8hd842-c.js +1 -0
  17. package/out/_next/static/chunks/02~o2nmo5pmy1.js +1 -0
  18. package/out/_next/static/chunks/07t.dhhokszz5.css +1 -0
  19. package/out/_next/static/chunks/0_wia9ofmsi1c.css +2 -0
  20. package/out/_next/static/chunks/0ah8fihozo2_u.js +5 -0
  21. package/out/_next/static/chunks/0bzupvr5gt3k9.js +31 -0
  22. package/out/_next/static/chunks/0d3shmwh5_nmn.js +1 -0
  23. package/out/_next/static/chunks/0e_h0d3ekzks8.css +1 -0
  24. package/out/_next/static/chunks/0gdluj423gso1.js +1 -0
  25. package/out/_next/static/chunks/0gmoiq06srjay.css +1 -0
  26. package/out/_next/static/chunks/0ho~log~~-jwp.css +1 -0
  27. package/out/_next/static/chunks/0imkasy7kb67u.js +1 -0
  28. package/out/_next/static/chunks/0jjc_b9q_ldi2.js +1 -0
  29. package/out/_next/static/chunks/0jl~j62iz2uvr.js +1 -0
  30. package/out/_next/static/chunks/0lqslm813wk_h.js +1 -0
  31. package/out/_next/static/chunks/{0bogtdbh.dcu1.js → 0n~dq4kpx9xxx.js} +1 -1
  32. package/out/_next/static/chunks/0pqt~8bl3ukh4.js +4 -0
  33. package/out/_next/static/chunks/0q782fxxd0lx~.js +1 -0
  34. package/out/_next/static/chunks/0qub_r0x_r-e9.css +1 -0
  35. package/out/_next/static/chunks/0slwj0c46k5cu.js +1 -0
  36. package/out/_next/static/chunks/0sorqk.oc6b7j.css +1 -0
  37. package/out/_next/static/chunks/0tapzqc6hgvx-.js +1 -0
  38. package/out/_next/static/chunks/0xsc7z5x8n7wg.js +1 -0
  39. package/out/_next/static/chunks/0zm~gys2jwl0g.js +1 -0
  40. package/out/_next/static/chunks/turbopack-0a_g3u0ud~jb8.js +1 -0
  41. package/out/_not-found/__next._full.txt +19 -15
  42. package/out/_not-found/__next._head.txt +3 -3
  43. package/out/_not-found/__next._index.txt +6 -5
  44. package/out/_not-found/__next._not-found.__PAGE__.txt +9 -0
  45. package/out/_not-found/__next._not-found.txt +3 -3
  46. package/out/_not-found/__next._tree.txt +2 -2
  47. package/out/_not-found/index.html +2 -2
  48. package/out/_not-found/index.txt +19 -15
  49. package/out/app/__next._full.txt +20 -0
  50. package/out/app/__next._head.txt +5 -0
  51. package/out/app/__next._index.txt +7 -0
  52. package/out/app/__next._tree.txt +2 -0
  53. package/out/app/__next.app.__PAGE__.txt +9 -0
  54. package/out/app/__next.app.txt +5 -0
  55. package/out/app/index.html +15 -0
  56. package/out/app/index.txt +20 -0
  57. package/out/changelog/__next._full.txt +22 -0
  58. package/out/changelog/__next._head.txt +5 -0
  59. package/out/changelog/__next._index.txt +7 -0
  60. package/out/changelog/__next._tree.txt +3 -0
  61. package/out/changelog/__next.changelog.__PAGE__.txt +10 -0
  62. package/out/changelog/__next.changelog.txt +5 -0
  63. package/out/changelog/index.html +15 -0
  64. package/out/changelog/index.txt +22 -0
  65. package/out/chat/__next._full.txt +20 -18
  66. package/out/chat/__next._head.txt +3 -3
  67. package/out/chat/__next._index.txt +6 -5
  68. package/out/chat/__next._tree.txt +3 -3
  69. package/out/chat/__next.chat.__PAGE__.txt +9 -0
  70. package/out/chat/__next.chat.txt +5 -4
  71. package/out/chat/index.html +2 -2
  72. package/out/chat/index.txt +20 -18
  73. package/out/docs/__next._full.txt +22 -0
  74. package/out/docs/__next._head.txt +5 -0
  75. package/out/docs/__next._index.txt +7 -0
  76. package/out/docs/__next._tree.txt +3 -0
  77. package/out/docs/__next.docs.__PAGE__.txt +10 -0
  78. package/out/docs/__next.docs.txt +5 -0
  79. package/out/docs/getting-started/__next._full.txt +22 -0
  80. package/out/docs/getting-started/__next._head.txt +5 -0
  81. package/out/docs/getting-started/__next._index.txt +7 -0
  82. package/out/docs/getting-started/__next._tree.txt +3 -0
  83. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +10 -0
  84. package/out/docs/getting-started/__next.docs.getting-started.txt +5 -0
  85. package/out/docs/getting-started/__next.docs.txt +5 -0
  86. package/out/docs/getting-started/index.html +15 -0
  87. package/out/docs/getting-started/index.txt +22 -0
  88. package/out/docs/index.html +15 -0
  89. package/out/docs/index.txt +22 -0
  90. package/out/download/__next._full.txt +34 -0
  91. package/out/download/__next._head.txt +5 -0
  92. package/out/download/__next._index.txt +7 -0
  93. package/out/download/__next._tree.txt +4 -0
  94. package/out/download/__next.download.__PAGE__.txt +16 -0
  95. package/out/download/__next.download.txt +5 -0
  96. package/out/download/index.html +15 -0
  97. package/out/download/index.txt +34 -0
  98. package/out/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  99. package/out/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  100. package/out/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  101. package/out/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
  102. package/out/index.html +2 -2
  103. package/out/index.txt +20 -17
  104. package/out/ping/__next._full.txt +21 -0
  105. package/out/ping/__next._head.txt +5 -0
  106. package/out/ping/__next._index.txt +7 -0
  107. package/out/ping/__next._tree.txt +4 -0
  108. package/out/ping/__next.ping.__PAGE__.txt +10 -0
  109. package/out/ping/__next.ping.txt +5 -0
  110. package/out/ping/index.html +15 -0
  111. package/out/ping/index.txt +21 -0
  112. package/out/pwa-512x512.png +0 -0
  113. package/out/web3/__next._full.txt +21 -0
  114. package/out/web3/__next._head.txt +5 -0
  115. package/out/web3/__next._index.txt +7 -0
  116. package/out/web3/__next._tree.txt +3 -0
  117. package/out/web3/__next.web3.__PAGE__.txt +9 -0
  118. package/out/web3/__next.web3.txt +6 -0
  119. package/out/web3/ed25519/__next._full.txt +20 -0
  120. package/out/web3/ed25519/__next._head.txt +5 -0
  121. package/out/web3/ed25519/__next._index.txt +7 -0
  122. package/out/web3/ed25519/__next._tree.txt +3 -0
  123. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +6 -0
  124. package/out/web3/ed25519/__next.web3.ed25519.txt +5 -0
  125. package/out/web3/ed25519/__next.web3.txt +6 -0
  126. package/out/web3/ed25519/index.html +1 -0
  127. package/out/web3/ed25519/index.txt +20 -0
  128. package/out/web3/index.html +15 -0
  129. package/out/web3/index.txt +21 -0
  130. package/out/web3/tools/__next._full.txt +20 -0
  131. package/out/web3/tools/__next._head.txt +5 -0
  132. package/out/web3/tools/__next._index.txt +7 -0
  133. package/out/web3/tools/__next._tree.txt +3 -0
  134. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +6 -0
  135. package/out/web3/tools/__next.web3.tools.txt +5 -0
  136. package/out/web3/tools/__next.web3.txt +6 -0
  137. package/out/web3/tools/index.html +1 -0
  138. package/out/web3/tools/index.txt +20 -0
  139. package/package.json +162 -53
  140. package/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  141. package/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  142. package/public/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0
  143. package/public/fonts/jetbrains-mono-latin-700-normal.woff2 +0 -0
  144. package/public/pwa-512x512.png +0 -0
  145. package/server/cli.js +3 -0
  146. package/server/index.js +995 -0
  147. package/{src → server/src}/config.js +51 -50
  148. package/{src → server/src}/core/cid.js +157 -150
  149. package/{src → server/src}/index.js +1952 -1669
  150. package/server/src/utils/api.js +68 -0
  151. package/server/src/utils/avatar.js +11 -0
  152. package/{src → server/src}/utils/errors.js +70 -66
  153. package/server/src/utils/mostWallet.js +42 -0
  154. package/server/src/utils/mp.js +105 -0
  155. package/{src → server/src}/utils/security.js +173 -169
  156. package/server/src/utils/userIdentity.js +93 -0
  157. package/cli.js +0 -2
  158. package/out/_next/static/chunks/00l-yd3t8dvwz.js +0 -5
  159. package/out/_next/static/chunks/03k8t3tgym~8~.js +0 -1
  160. package/out/_next/static/chunks/09vfh8lfuacc0.css +0 -1
  161. package/out/_next/static/chunks/0dbhjjzl8qfwv.js +0 -1
  162. package/out/_next/static/chunks/0f73psqhr8dre.css +0 -1
  163. package/out/_next/static/chunks/0fbi7z4_.4j1j.js +0 -1
  164. package/out/_next/static/chunks/0ht900cau6_ur.js +0 -31
  165. package/out/_next/static/chunks/0ohm.ia-4ec60.js +0 -1
  166. package/out/_next/static/chunks/0u5ydb-f0.vxl.js +0 -1
  167. package/out/_next/static/chunks/14t2m1on-s5v~.js +0 -1
  168. package/out/_next/static/chunks/turbopack-076ce9exut_h3.js +0 -1
  169. package/out/_not-found/__next._not-found/__PAGE__.txt +0 -5
  170. package/out/app.css +0 -1535
  171. package/out/bundle.js +0 -107
  172. package/out/bundle.js.map +0 -7
  173. package/out/chat/__next.chat/__PAGE__.txt +0 -9
  174. package/out/chat-page.js +0 -112
  175. package/out/chat.css +0 -378
  176. package/out/index.js +0 -148
  177. package/public/app.css +0 -1535
  178. package/public/bundle.js +0 -107
  179. package/public/bundle.js.map +0 -7
  180. package/public/chat-page.js +0 -112
  181. package/public/chat.css +0 -378
  182. package/public/index.js +0 -148
  183. package/server.js +0 -880
  184. package/src/utils/api.js +0 -6
  185. /package/out/_next/static/{0h4f4QFk_KC9FlSRfQACk → sV38nXrv3xVVO6wvSdFlZ}/_buildManifest.js +0 -0
  186. /package/out/_next/static/{0h4f4QFk_KC9FlSRfQACk → sV38nXrv3xVVO6wvSdFlZ}/_clientMiddlewareManifest.js +0 -0
  187. /package/out/_next/static/{0h4f4QFk_KC9FlSRfQACk → sV38nXrv3xVVO6wvSdFlZ}/_ssgManifest.js +0 -0
@@ -0,0 +1,995 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { spawn } from 'node:child_process'
6
+ import Busboy from 'busboy'
7
+ import { WebSocketServer } from 'ws'
8
+ import { Hono } from 'hono'
9
+ import { cors } from 'hono/cors'
10
+ import { serveStatic } from '@hono/node-server/serve-static'
11
+ import { serve } from '@hono/node-server'
12
+ import { MostBoxEngine } from './src/index.js'
13
+ import { parseMostLink, validateCidString } from './src/core/cid.js'
14
+ import { sanitizeFilename } from './src/utils/security.js'
15
+ import { MAX_FILE_SIZE } from './src/config.js'
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const PORT = Number(process.env.MOSTBOX_PORT || process.env.PORT) || 1976
19
+ const HOST = process.env.MOSTBOX_HOST || '0.0.0.0'
20
+
21
+ const MAX_UPLOAD_SIZE = MAX_FILE_SIZE
22
+ const UPLOAD_TMP_DIR = path.join(os.tmpdir(), 'most-box-uploads')
23
+
24
+ const RATE_LIMIT_WINDOW = 60 * 1000
25
+ const RATE_LIMIT_MAX_REQUESTS = 120
26
+
27
+ // --- 配置 ---
28
+ const CONFIG_DIR = path.join(os.homedir(), '.most-box')
29
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
30
+
31
+ function loadConfig() {
32
+ try {
33
+ if (fs.existsSync(CONFIG_FILE)) {
34
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'))
35
+ }
36
+ } catch (err) {
37
+ console.error('[Config] Load error:', err.message)
38
+ }
39
+ return {}
40
+ }
41
+
42
+ function saveConfig(config) {
43
+ try {
44
+ if (!fs.existsSync(CONFIG_DIR)) {
45
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
46
+ }
47
+ const tmpPath = CONFIG_FILE + '.tmp'
48
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8')
49
+ fs.renameSync(tmpPath, CONFIG_FILE)
50
+ return true
51
+ } catch (err) {
52
+ console.error('[Config] Save error:', err.message)
53
+ return false
54
+ }
55
+ }
56
+
57
+ function getDataPath() {
58
+ if (process.env.MOSTBOX_DATA_PATH) {
59
+ return process.env.MOSTBOX_DATA_PATH
60
+ }
61
+ const config = loadConfig()
62
+ return config.dataPath || path.join(os.homedir(), 'most-data')
63
+ }
64
+
65
+ // --- 静态文件服务 ---
66
+ const MIME_TYPES = {
67
+ '.html': 'text/html; charset=utf-8',
68
+ '.js': 'application/javascript; charset=utf-8',
69
+ '.css': 'text/css; charset=utf-8',
70
+ '.json': 'application/json',
71
+ '.png': 'image/png',
72
+ '.jpg': 'image/jpeg',
73
+ '.jpeg': 'image/jpeg',
74
+ '.gif': 'image/gif',
75
+ '.webp': 'image/webp',
76
+ '.svg': 'image/svg+xml',
77
+ '.ico': 'image/x-icon',
78
+ '.mp4': 'video/mp4',
79
+ '.webm': 'video/webm',
80
+ '.ogg': 'video/ogg',
81
+ '.mp3': 'audio/mpeg',
82
+ '.wav': 'audio/wav',
83
+ '.flac': 'audio/flac',
84
+ '.aac': 'audio/aac',
85
+ '.m4a': 'audio/mp4',
86
+ '.opus': 'audio/opus',
87
+ '.woff2': 'font/woff2',
88
+ '.woff': 'font/woff',
89
+ }
90
+
91
+ function getMimeType(fileName) {
92
+ const ext = path.extname(fileName).toLowerCase()
93
+ return MIME_TYPES[ext] || 'application/octet-stream'
94
+ }
95
+
96
+ function decodeFilenameFromHeader(headerStr) {
97
+ if (!headerStr) return null
98
+
99
+ const filenameStarMatch = headerStr.match(
100
+ /filename\*=(?:UTF-8''|utf-8'')([^;\r\n]+)/i
101
+ )
102
+ if (filenameStarMatch) {
103
+ return decodeURIComponent(filenameStarMatch[1])
104
+ }
105
+
106
+ const filenameMatch = headerStr.match(/filename="([^"]+)"/)
107
+ if (filenameMatch) {
108
+ const rawFilename = filenameMatch[1]
109
+ try {
110
+ const buf = Buffer.from(rawFilename, 'latin1')
111
+ const decoded = buf.toString('utf8')
112
+ if (decoded.includes('\ufffd')) {
113
+ return rawFilename
114
+ }
115
+ return decoded
116
+ } catch {
117
+ return rawFilename
118
+ }
119
+ }
120
+
121
+ const filenamePlainMatch = headerStr.match(/filename=([^;\r\n]+)/)
122
+ if (filenamePlainMatch) {
123
+ return filenamePlainMatch[1].trim()
124
+ }
125
+ return null
126
+ }
127
+
128
+ async function parseMultipartBusboy(req) {
129
+ return new Promise((resolve, reject) => {
130
+ if (!fs.existsSync(UPLOAD_TMP_DIR)) {
131
+ fs.mkdirSync(UPLOAD_TMP_DIR, { recursive: true })
132
+ }
133
+
134
+ const busboy = Busboy({
135
+ headers: req.headers,
136
+ limits: {
137
+ fileSize: MAX_UPLOAD_SIZE,
138
+ files: 1,
139
+ fields: 0,
140
+ },
141
+ })
142
+
143
+ const result = { filePath: null, filename: null }
144
+ let fileSize = 0
145
+ let writeStream = null
146
+ let tempPath = null
147
+
148
+ busboy.on('file', (name, stream, info) => {
149
+ result.filename = decodeFilenameFromHeader(`filename="${info.filename}"`)
150
+ tempPath = path.join(
151
+ UPLOAD_TMP_DIR,
152
+ `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
153
+ )
154
+ writeStream = fs.createWriteStream(tempPath)
155
+
156
+ stream.on('data', chunk => {
157
+ fileSize += chunk.length
158
+ if (fileSize > MAX_UPLOAD_SIZE) {
159
+ stream.destroy()
160
+ writeStream.destroy()
161
+ fs.unlink(tempPath, () => {})
162
+ reject(new Error('File too large'))
163
+ return
164
+ }
165
+ })
166
+
167
+ stream.on('error', () => {
168
+ if (tempPath) fs.unlink(tempPath, () => {})
169
+ })
170
+
171
+ stream.pipe(writeStream)
172
+
173
+ writeStream.on('finish', () => {
174
+ result.filePath = tempPath
175
+ resolve(result)
176
+ })
177
+
178
+ writeStream.on('error', err => {
179
+ if (tempPath) fs.unlink(tempPath, () => {})
180
+ reject(err)
181
+ })
182
+ })
183
+
184
+ busboy.on('error', err => {
185
+ if (tempPath) fs.unlink(tempPath, () => {})
186
+ reject(err)
187
+ })
188
+
189
+ busboy.on('close', () => {
190
+ if (!result.filename) {
191
+ resolve(null)
192
+ }
193
+ })
194
+
195
+ req.on('error', err => {
196
+ if (tempPath) fs.unlink(tempPath, () => {})
197
+ reject(err)
198
+ })
199
+ req.pipe(busboy)
200
+ })
201
+ }
202
+
203
+ // --- Hono 应用工厂 ---
204
+ export function createApp(engine, options = {}) {
205
+ const appPort = options.port || PORT
206
+ const wssRef = options.wssRef || { current: null }
207
+ const serverInstanceRef = options.serverInstanceRef || { current: null }
208
+
209
+ // 速率限制(每个 app 实例独立)
210
+ const rateLimitMap = new Map()
211
+ function checkRateLimit(clientIp) {
212
+ const now = Date.now()
213
+ if (!rateLimitMap.has(clientIp)) {
214
+ rateLimitMap.set(clientIp, [])
215
+ }
216
+ const requests = rateLimitMap.get(clientIp)
217
+ while (requests.length > 0 && requests[0] < now - RATE_LIMIT_WINDOW) {
218
+ requests.shift()
219
+ }
220
+ if (requests.length === 0) {
221
+ rateLimitMap.delete(clientIp)
222
+ }
223
+ if (requests.length >= RATE_LIMIT_MAX_REQUESTS) {
224
+ return false
225
+ }
226
+ requests.push(now)
227
+ return true
228
+ }
229
+
230
+ function rateLimitMiddleware() {
231
+ return async (c, next) => {
232
+ const clientIp =
233
+ c.req.header('x-forwarded-for') ||
234
+ c.env.incoming?.socket?.remoteAddress ||
235
+ 'unknown'
236
+ if (!checkRateLimit(clientIp)) {
237
+ return c.json({ error: 'Too many requests' }, 429)
238
+ }
239
+ await next()
240
+ }
241
+ }
242
+
243
+ // WebSocket 广播
244
+ const channelSubscriptions = new Map()
245
+
246
+ function wsBroadcast(event, data) {
247
+ const payload = JSON.stringify({ event, data })
248
+ const wss = wssRef.current
249
+ if (wss) {
250
+ wss.clients.forEach(client => {
251
+ if (client.readyState === 1) {
252
+ try {
253
+ client.send(payload)
254
+ } catch (err) {
255
+ console.warn('[WS] Failed to send to client:', err.message)
256
+ }
257
+ }
258
+ })
259
+ }
260
+ }
261
+
262
+ function wsSendToChannel(channelName, event, data) {
263
+ const payload = JSON.stringify({ event, data })
264
+ const subscribers = channelSubscriptions.get(channelName)
265
+ if (subscribers) {
266
+ subscribers.forEach(ws => {
267
+ if (ws.readyState === 1) {
268
+ try {
269
+ ws.send(payload)
270
+ } catch (err) {
271
+ console.warn('[WS] Failed to send to channel subscriber:', err.message)
272
+ }
273
+ }
274
+ })
275
+ }
276
+ }
277
+
278
+ function subscribeToChannel(ws, channelName) {
279
+ if (!channelSubscriptions.has(channelName)) {
280
+ channelSubscriptions.set(channelName, new Set())
281
+ }
282
+ channelSubscriptions.get(channelName).add(ws)
283
+ }
284
+
285
+ function unsubscribeFromChannel(ws, channelName) {
286
+ const subscribers = channelSubscriptions.get(channelName)
287
+ if (subscribers) {
288
+ subscribers.delete(ws)
289
+ if (subscribers.size === 0) {
290
+ channelSubscriptions.delete(channelName)
291
+ }
292
+ }
293
+ }
294
+
295
+ function cleanupWsSubscriptions(ws) {
296
+ for (const [channel, subscribers] of channelSubscriptions) {
297
+ subscribers.delete(ws)
298
+ if (subscribers.size === 0) {
299
+ channelSubscriptions.delete(channel)
300
+ }
301
+ }
302
+ }
303
+
304
+ // 将广播函数挂载到 engine 上供外部测试使用
305
+ engine.wsBroadcast = wsBroadcast
306
+ engine.wsSendToChannel = wsSendToChannel
307
+
308
+ const app = new Hono()
309
+
310
+ // CORS 中间件
311
+ app.use(
312
+ '/api/*',
313
+ cors({
314
+ origin: [
315
+ 'http://localhost:3000',
316
+ 'https://most.box',
317
+ `http://localhost:${appPort}`,
318
+ ],
319
+ credentials: true,
320
+ })
321
+ )
322
+
323
+ // 速率限制中间件
324
+ app.use('/api/*', rateLimitMiddleware())
325
+
326
+ // 全局错误处理
327
+ app.onError((err, c) => {
328
+ console.error('[API Error]', err)
329
+ try {
330
+ const errorLogPath = path.join(CONFIG_DIR, 'server-error.log')
331
+ if (!fs.existsSync(CONFIG_DIR)) {
332
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
333
+ }
334
+ fs.appendFileSync(
335
+ errorLogPath,
336
+ `[${new Date().toISOString()}] ${err.stack}\n`
337
+ )
338
+ } catch {}
339
+ return c.json({ error: err.message, code: err.code }, 500)
340
+ })
341
+
342
+ // --- 配置路由 ---
343
+ app.get('/api/node-id', c => {
344
+ return c.json({ id: engine.getNodeId() })
345
+ })
346
+
347
+ app.get('/api/config', c => {
348
+ const config = loadConfig()
349
+ return c.json({ dataPath: config.dataPath || '' })
350
+ })
351
+
352
+ app.post('/api/config', async c => {
353
+ const body = await c.req.json()
354
+ const config = loadConfig()
355
+
356
+ if (body.resetStorage) {
357
+ config.dataPath = ''
358
+ } else if (body.dataPath !== undefined) {
359
+ let dataPath = body.dataPath.trim()
360
+ let basePath = dataPath
361
+
362
+ if (dataPath.match(/^[A-Za-z]:\\$/)) {
363
+ basePath = dataPath
364
+ dataPath = path.join(dataPath, 'most-data')
365
+ }
366
+
367
+ if (!fs.existsSync(basePath)) {
368
+ return c.json({ error: '目录不存在' }, 400)
369
+ }
370
+
371
+ if (!fs.existsSync(dataPath)) {
372
+ fs.mkdirSync(dataPath, { recursive: true })
373
+ }
374
+
375
+ config.dataPath = dataPath
376
+ }
377
+
378
+ const success = saveConfig(config)
379
+ return c.json({ success, dataPath: getDataPath() })
380
+ })
381
+
382
+ app.get('/api/config/data-path', c => {
383
+ const config = loadConfig()
384
+ const isDefault = !config.dataPath
385
+ const dataPath = getDataPath()
386
+ return c.json({ dataPath, isDefault })
387
+ })
388
+
389
+ // --- 网络路由 ---
390
+ app.get('/api/network-status', c => {
391
+ return c.json(engine.getNetworkStatus())
392
+ })
393
+
394
+ app.get('/api/network', c => {
395
+ const interfaces = os.networkInterfaces()
396
+ const addresses = []
397
+ const seen = new Set()
398
+
399
+ for (const [name, nets] of Object.entries(interfaces)) {
400
+ for (const net of nets) {
401
+ if (net.family !== 'IPv4' || net.internal) continue
402
+ if (seen.has(net.address)) continue
403
+ seen.add(net.address)
404
+
405
+ let type = 'lan'
406
+ let label = '局域网'
407
+ if (net.address.startsWith('100.')) {
408
+ type = 'tailscale'
409
+ label = 'Tailscale'
410
+ } else if (
411
+ name.toLowerCase().includes('zt') ||
412
+ name.toLowerCase().includes('zerotier')
413
+ ) {
414
+ type = 'zerotier'
415
+ label = 'ZeroTier'
416
+ }
417
+
418
+ addresses.push({ type, ip: net.address, label, iface: name })
419
+ }
420
+ }
421
+
422
+ const localEntry = {
423
+ type: 'local',
424
+ ip: 'localhost',
425
+ label: '本机',
426
+ iface: 'loopback',
427
+ }
428
+ return c.json({ port: appPort, addresses: [localEntry, ...addresses] })
429
+ })
430
+
431
+ // --- 文件路由 ---
432
+ app.get('/api/files', c => {
433
+ return c.json(engine.listPublishedFiles())
434
+ })
435
+
436
+ app.post('/api/publish', async c => {
437
+ const req = c.env.incoming
438
+ const result = await parseMultipartBusboy(req)
439
+
440
+ if (!result || !result.filename) {
441
+ return c.json({ error: 'No file provided' }, 400)
442
+ }
443
+
444
+ try {
445
+ const publishResult = await engine.publishFile(
446
+ result.filePath,
447
+ result.filename
448
+ )
449
+ return c.json({ success: true, ...publishResult })
450
+ } finally {
451
+ fs.unlink(result.filePath, () => {})
452
+ }
453
+ })
454
+
455
+ app.post('/api/download', async c => {
456
+ const body = await c.req.json()
457
+ if (!body.link) {
458
+ return c.json({ error: 'link is required' }, 400)
459
+ }
460
+
461
+ const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
462
+
463
+ const parsed = parseMostLink(body.link)
464
+ if (parsed.error) {
465
+ return c.json({ error: parsed.error }, 400)
466
+ }
467
+
468
+ const existingFile = engine
469
+ .getPublishedFiles()
470
+ .find(f => f.cid === parsed.cid)
471
+ if (existingFile) {
472
+ console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
473
+ return c.json({
474
+ success: true,
475
+ taskId,
476
+ alreadyExists: true,
477
+ fileName: existingFile.fileName,
478
+ })
479
+ }
480
+
481
+ engine.downloadFile(body.link, taskId).catch(err => {
482
+ if (err.message === 'Download cancelled') {
483
+ wsBroadcast('download:cancelled', { taskId })
484
+ } else {
485
+ wsBroadcast('download:error', { taskId, error: err.message })
486
+ }
487
+ })
488
+
489
+ return c.json({ success: true, taskId })
490
+ })
491
+
492
+ app.post('/api/download/cancel', async c => {
493
+ const body = await c.req.json()
494
+ if (!body.taskId) {
495
+ return c.json({ error: 'taskId is required' }, 400)
496
+ }
497
+ engine.cancelDownload(body.taskId)
498
+ return c.json({ success: true })
499
+ })
500
+
501
+ app.delete('/api/files/:cid', async c => {
502
+ const cid = c.req.param('cid')
503
+ const cidValidation = validateCidString(cid)
504
+ if (!cidValidation.valid) {
505
+ return c.json({ error: cidValidation.error }, 400)
506
+ }
507
+ const result = await engine.deletePublishedFile(cid)
508
+ return c.json(result)
509
+ })
510
+
511
+ app.post('/api/move', async c => {
512
+ const body = await c.req.json()
513
+ if (!body.cid || !body.newFileName) {
514
+ return c.json({ error: 'cid and newFileName are required' }, 400)
515
+ }
516
+ const cidValidation = validateCidString(body.cid)
517
+ if (!cidValidation.valid) {
518
+ return c.json({ error: cidValidation.error }, 400)
519
+ }
520
+ const cleanFileName = sanitizeFilename(body.newFileName)
521
+ if (
522
+ !cleanFileName ||
523
+ cleanFileName === 'unnamed' ||
524
+ body.newFileName.length > 255
525
+ ) {
526
+ return c.json({ error: 'Invalid filename' }, 400)
527
+ }
528
+ try {
529
+ const result = engine.moveFile(body.cid, cleanFileName)
530
+ return c.json({ success: true, ...result })
531
+ } catch (err) {
532
+ return c.json({ error: err.message }, 400)
533
+ }
534
+ })
535
+
536
+ app.get('/api/files/:cid/download', async c => {
537
+ const cid = c.req.param('cid')
538
+ const cidValidation = validateCidString(cid)
539
+ if (!cidValidation.valid) {
540
+ return c.json({ error: cidValidation.error }, 400)
541
+ }
542
+
543
+ const rangeHeader = c.req.header('range')
544
+
545
+ try {
546
+ if (rangeHeader) {
547
+ const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
548
+ if (rangeMatch) {
549
+ const start = parseInt(rangeMatch[1], 10)
550
+ const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
551
+ const offset = start
552
+ const limit = end !== undefined ? end - start + 1 : undefined
553
+
554
+ const result = await engine.readFileRaw(cid, { offset, limit })
555
+ const contentType = getMimeType(result.fileName)
556
+
557
+ c.header('Content-Type', contentType)
558
+ c.header('Content-Length', String(result.buffer.length))
559
+ c.header(
560
+ 'Content-Range',
561
+ `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`
562
+ )
563
+ c.header('Accept-Ranges', 'bytes')
564
+ c.status(206)
565
+ return c.body(result.buffer)
566
+ }
567
+ }
568
+
569
+ const result = await engine.readFileRaw(cid)
570
+ const contentType = getMimeType(result.fileName)
571
+ c.header('Content-Type', contentType)
572
+ c.header('Content-Length', String(result.totalSize))
573
+ c.header('Accept-Ranges', 'bytes')
574
+ c.header(
575
+ 'Content-Disposition',
576
+ `inline; filename="${encodeURIComponent(result.fileName)}"`
577
+ )
578
+ return c.body(result.buffer)
579
+ } catch (err) {
580
+ if (err.message === 'File not found') {
581
+ return c.json({ error: err.message }, 404)
582
+ }
583
+ return c.json({ error: err.message }, 400)
584
+ }
585
+ })
586
+
587
+ // --- 回收站路由 ---
588
+ app.get('/api/trash', c => {
589
+ return c.json(engine.listTrashFiles())
590
+ })
591
+
592
+ app.post('/api/trash/:cid/restore', async c => {
593
+ const cid = c.req.param('cid')
594
+ const cidValidation = validateCidString(cid)
595
+ if (!cidValidation.valid) {
596
+ return c.json({ error: cidValidation.error }, 400)
597
+ }
598
+ try {
599
+ const result = engine.restoreTrashFile(cid)
600
+ return c.json({ success: true, files: result })
601
+ } catch (err) {
602
+ return c.json({ error: err.message }, 400)
603
+ }
604
+ })
605
+
606
+ app.delete('/api/trash/:cid', async c => {
607
+ const cid = c.req.param('cid')
608
+ const cidValidation = validateCidString(cid)
609
+ if (!cidValidation.valid) {
610
+ return c.json({ error: cidValidation.error }, 400)
611
+ }
612
+ const result = await engine.permanentDeleteTrashFile(cid)
613
+ return c.json({ success: true, trashFiles: result })
614
+ })
615
+
616
+ app.delete('/api/trash', async c => {
617
+ const result = await engine.emptyTrash()
618
+ return c.json({ success: true, trashFiles: result })
619
+ })
620
+
621
+ // --- 收藏路由 ---
622
+ app.post('/api/files/:cid/star', async c => {
623
+ const cid = c.req.param('cid')
624
+ const cidValidation = validateCidString(cid)
625
+ if (!cidValidation.valid) {
626
+ return c.json({ error: cidValidation.error }, 400)
627
+ }
628
+ try {
629
+ const result = engine.toggleStarred(cid)
630
+ return c.json({ success: true, ...result })
631
+ } catch (err) {
632
+ return c.json({ error: err.message }, 400)
633
+ }
634
+ })
635
+
636
+ // --- 存储路由 ---
637
+ app.get('/api/storage', async c => {
638
+ const result = await engine.getStorageStats()
639
+ return c.json(result)
640
+ })
641
+
642
+ // --- 显示名路由 ---
643
+ app.get('/api/display-name', c => {
644
+ return c.json({ displayName: engine.getDisplayName() })
645
+ })
646
+
647
+ app.post('/api/display-name', async c => {
648
+ const body = await c.req.json()
649
+ if (!body.name || !body.name.trim()) {
650
+ return c.json({ error: 'name is required' }, 400)
651
+ }
652
+ const trimmed = body.name.trim()
653
+ if (trimmed.length > 100) {
654
+ return c.json({ error: 'Name too long (max 100 chars)' }, 400)
655
+ }
656
+ if (/[<>]/.test(trimmed)) {
657
+ return c.json({ error: 'Name contains invalid characters' }, 400)
658
+ }
659
+ const success = engine.setDisplayName(trimmed)
660
+ return c.json({ success, displayName: engine.getDisplayName() })
661
+ })
662
+
663
+ // --- 频道路由 ---
664
+ app.post('/api/channels', async c => {
665
+ const body = await c.req.json()
666
+ if (!body.name || !body.name.trim()) {
667
+ return c.json({ error: 'name is required' }, 400)
668
+ }
669
+ try {
670
+ const result = await engine.createChannel(
671
+ body.name.trim(),
672
+ body.type || 'personal'
673
+ )
674
+ return c.json({ success: true, ...result })
675
+ } catch (err) {
676
+ return c.json({ error: err.message }, 400)
677
+ }
678
+ })
679
+
680
+ app.get('/api/channels', c => {
681
+ return c.json(engine.listChannels())
682
+ })
683
+
684
+ app.delete('/api/channels/:name', async c => {
685
+ const name = c.req.param('name')
686
+ try {
687
+ const result = await engine.leaveChannel(name)
688
+ return c.json({ success: true, channels: result })
689
+ } catch (err) {
690
+ return c.json({ error: err.message }, 400)
691
+ }
692
+ })
693
+
694
+ app.get('/api/channels/:name/messages', async c => {
695
+ const name = c.req.param('name')
696
+ const limit = parseInt(c.req.query('limit') || '100', 10)
697
+ const offset = parseInt(c.req.query('offset') || '0', 10)
698
+ try {
699
+ const messages = await engine.getChannelMessages(name, { limit, offset })
700
+ return c.json(messages)
701
+ } catch (err) {
702
+ return c.json({ error: err.message }, 400)
703
+ }
704
+ })
705
+
706
+ app.post('/api/channels/:name/messages', async c => {
707
+ const name = c.req.param('name')
708
+ const body = await c.req.json()
709
+ if (!body.content || !body.content.trim()) {
710
+ return c.json({ error: 'content is required' }, 400)
711
+ }
712
+ if (!body.author || !body.authorName) {
713
+ return c.json({ error: 'author and authorName are required' }, 400)
714
+ }
715
+ if (!/^0x[a-fA-F0-9]{40}$/.test(body.author)) {
716
+ return c.json({ error: 'Invalid author format' }, 400)
717
+ }
718
+ if (body.authorName.length > 50) {
719
+ return c.json({ error: 'authorName too long' }, 400)
720
+ }
721
+ try {
722
+ const message = await engine.sendMessage(
723
+ name,
724
+ body.content,
725
+ body.author,
726
+ body.authorName
727
+ )
728
+ return c.json({ success: true, message })
729
+ } catch (err) {
730
+ return c.json({ error: err.message }, 400)
731
+ }
732
+ })
733
+
734
+ app.get('/api/channels/:name/peers', c => {
735
+ return c.json(engine.getChannelPeers(c.req.param('name')))
736
+ })
737
+
738
+ // --- 文件夹重命名 ---
739
+ app.post('/api/folder/rename', async c => {
740
+ const body = await c.req.json()
741
+ if (!body.oldPath || !body.newPath) {
742
+ return c.json({ error: 'oldPath and newPath are required' }, 400)
743
+ }
744
+ if (body.oldPath.length > 500 || body.newPath.length > 500) {
745
+ return c.json({ error: 'Path too long' }, 400)
746
+ }
747
+ if (body.oldPath.includes('..') || body.newPath.includes('..')) {
748
+ return c.json({ error: 'Path traversal not allowed' }, 400)
749
+ }
750
+ try {
751
+ const result = engine.renameFolder(body.oldPath, body.newPath)
752
+ return c.json({ success: true, ...result })
753
+ } catch (err) {
754
+ return c.json({ error: err.message }, 400)
755
+ }
756
+ })
757
+
758
+ // --- 关机路由 ---
759
+ app.post('/api/shutdown', c => {
760
+ const clientIp = c.env.incoming?.socket?.remoteAddress || 'unknown'
761
+ const isLocalhost =
762
+ clientIp === 'localhost' ||
763
+ clientIp === '::1' ||
764
+ clientIp === '::ffff:localhost' ||
765
+ clientIp === '127.0.0.1' ||
766
+ clientIp === '::ffff:127.0.0.1' ||
767
+ clientIp.startsWith('::ffff:127.')
768
+ if (!isLocalhost) {
769
+ return c.json({ error: 'Forbidden' }, 403)
770
+ }
771
+ c.json({ success: true })
772
+ console.log('[MostBox] Shutdown requested via API...')
773
+ setTimeout(async () => {
774
+ await engine.stop()
775
+ if (serverInstanceRef.current) serverInstanceRef.current.close()
776
+ console.log('[MostBox] Server stopped.')
777
+ process.exit(0)
778
+ }, 100)
779
+ return c.body(null)
780
+ })
781
+
782
+ // --- 静态文件服务(SPA fallback) ---
783
+ const publicDir = path.join(__dirname, '..', 'out')
784
+
785
+ app.get('/static/*', serveStatic({ root: './out' }))
786
+ app.get('/_next/*', serveStatic({ root: './out' }))
787
+
788
+ app.all('/api/*', c => {
789
+ return c.json({ error: 'Not found' }, 404)
790
+ })
791
+
792
+ app.get('*', async c => {
793
+ const pathname = c.req.path
794
+ const filePath = path.join(publicDir, pathname)
795
+ const resolved = path.resolve(filePath)
796
+ const resolvedPublic = path.resolve(publicDir)
797
+
798
+ if (!resolved.startsWith(resolvedPublic + path.sep) && resolved !== resolvedPublic) {
799
+ return c.json({ error: 'Not found' }, 404)
800
+ }
801
+
802
+ if (fs.existsSync(filePath)) {
803
+ const stat = fs.statSync(filePath)
804
+ if (stat.isFile()) {
805
+ const ext = path.extname(filePath)
806
+ c.header('Content-Type', MIME_TYPES[ext] || 'application/octet-stream')
807
+ return c.body(fs.readFileSync(filePath))
808
+ }
809
+ if (stat.isDirectory()) {
810
+ const dirIndex = path.join(filePath, 'index.html')
811
+ if (fs.existsSync(dirIndex)) {
812
+ c.header('Content-Type', 'text/html; charset=utf-8')
813
+ return c.body(fs.readFileSync(dirIndex, 'utf-8'))
814
+ }
815
+ }
816
+ }
817
+
818
+ const indexPath = path.join(publicDir, 'index.html')
819
+ if (fs.existsSync(indexPath)) {
820
+ c.header('Content-Type', 'text/html; charset=utf-8')
821
+ return c.body(fs.readFileSync(indexPath, 'utf-8'))
822
+ }
823
+
824
+ return c.json({ error: 'Not found' }, 404)
825
+ })
826
+
827
+ return {
828
+ app,
829
+ wsBroadcast,
830
+ wsSendToChannel,
831
+ subscribeToChannel,
832
+ unsubscribeFromChannel,
833
+ cleanupWsSubscriptions,
834
+ }
835
+ }
836
+
837
+ // --- 主函数 ---
838
+ export async function main() {
839
+ console.log('[MostBox] Starting core daemon...')
840
+
841
+ if (fs.existsSync(UPLOAD_TMP_DIR)) {
842
+ const staleFiles = fs.readdirSync(UPLOAD_TMP_DIR)
843
+ for (const file of staleFiles) {
844
+ try {
845
+ fs.unlinkSync(path.join(UPLOAD_TMP_DIR, file))
846
+ } catch (err) {
847
+ console.warn('[MostBox] Failed to clean upload temp file:', err.message)
848
+ }
849
+ }
850
+ console.log(
851
+ `[MostBox] Cleaned ${staleFiles.length} stale upload temp files`
852
+ )
853
+ }
854
+
855
+ const dataPath = getDataPath()
856
+ console.log(`[MostBox] Storage: ${dataPath}`)
857
+
858
+ const engine = new MostBoxEngine({ dataPath })
859
+
860
+ const wssRef = { current: null }
861
+ const serverInstanceRef = { current: null }
862
+
863
+ const {
864
+ app,
865
+ wsBroadcast,
866
+ wsSendToChannel,
867
+ subscribeToChannel,
868
+ unsubscribeFromChannel,
869
+ cleanupWsSubscriptions,
870
+ } = createApp(engine, {
871
+ port: PORT,
872
+ wssRef,
873
+ serverInstanceRef,
874
+ })
875
+
876
+ engine.on('download:progress', data => wsBroadcast('download:progress', data))
877
+ engine.on('download:status', data => wsBroadcast('download:status', data))
878
+ engine.on('download:success', data => wsBroadcast('download:success', data))
879
+ engine.on('download:cancelled', data =>
880
+ wsBroadcast('download:cancelled', data)
881
+ )
882
+ engine.on('publish:progress', data => wsBroadcast('publish:progress', data))
883
+ engine.on('publish:success', data => wsBroadcast('publish:success', data))
884
+ engine.on('connection', () => {
885
+ wsBroadcast('network:status', engine.getNetworkStatus())
886
+ })
887
+ engine.on('channel:message', data =>
888
+ wsSendToChannel(data.channel, 'channel:message', data)
889
+ )
890
+ engine.on('channel:peer:online', data =>
891
+ wsBroadcast('channel:peer:online', data)
892
+ )
893
+ engine.on('channel:peer:offline', data =>
894
+ wsBroadcast('channel:peer:offline', data)
895
+ )
896
+ engine.on('channel:joined', data => wsBroadcast('channel:joined', data))
897
+ engine.on('channel:left', data => wsBroadcast('channel:left', data))
898
+
899
+ await engine.start()
900
+ console.log('[MostBox] Engine ready')
901
+
902
+ serverInstanceRef.current = serve(
903
+ { fetch: app.fetch, port: PORT, hostname: HOST },
904
+ () => {
905
+ const displayUrl = `http://localhost:${PORT}`
906
+ console.log(`[MostBox] Server running at ${displayUrl}`)
907
+
908
+ if (process.env.ELECTRON_APP) return
909
+
910
+ if (process.platform === 'win32') {
911
+ spawn('cmd.exe', ['/c', 'start', '', displayUrl], {
912
+ detached: true,
913
+ stdio: 'ignore',
914
+ }).unref()
915
+ } else {
916
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
917
+ spawn(cmd, [displayUrl], {
918
+ detached: true,
919
+ stdio: 'ignore',
920
+ }).unref()
921
+ }
922
+ }
923
+ )
924
+
925
+ wssRef.current = new WebSocketServer({ noServer: true })
926
+ wssRef.current.on('connection', ws => {
927
+ ws.on('error', () => {})
928
+ ws.on('close', () => {
929
+ cleanupWsSubscriptions(ws)
930
+ })
931
+ ws.on('message', raw => {
932
+ try {
933
+ const msg = JSON.parse(raw)
934
+ const { event, data } = msg
935
+
936
+ switch (event) {
937
+ case 'register':
938
+ ws.peerId = data.peerId
939
+ break
940
+ case 'channel:subscribe':
941
+ if (data.channel) {
942
+ subscribeToChannel(ws, data.channel)
943
+ }
944
+ break
945
+ case 'channel:unsubscribe':
946
+ if (data.channel) {
947
+ unsubscribeFromChannel(ws, data.channel)
948
+ }
949
+ break
950
+ }
951
+ } catch (err) {
952
+ console.error('[WS Message Error]', err.message)
953
+ }
954
+ })
955
+ })
956
+
957
+ serverInstanceRef.current.on('upgrade', (req, socket, head) => {
958
+ if (req.url.startsWith('/ws')) {
959
+ wssRef.current.handleUpgrade(req, socket, head, ws => {
960
+ wssRef.current.emit('connection', ws, req)
961
+ })
962
+ } else {
963
+ socket.destroy()
964
+ }
965
+ })
966
+
967
+ process.on('SIGINT', async () => {
968
+ console.log('\n[MostBox] Shutting down...')
969
+ await engine.stop()
970
+ if (wssRef.current) wssRef.current.close()
971
+ serverInstanceRef.current.close()
972
+ process.exit(0)
973
+ })
974
+
975
+ process.on('SIGTERM', async () => {
976
+ await engine.stop()
977
+ if (wssRef.current) wssRef.current.close()
978
+ serverInstanceRef.current.close()
979
+ process.exit(0)
980
+ })
981
+
982
+ return engine
983
+ }
984
+
985
+ // 仅在直接运行时执行 main(通过 node server/index.js 或 CLI)
986
+ const isMain =
987
+ process.argv[1] &&
988
+ (import.meta.url === new URL(process.argv[1], 'file://').href ||
989
+ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]))
990
+ if (isMain) {
991
+ main().catch(err => {
992
+ console.error('[MostBox] Fatal error:', err)
993
+ process.exit(1)
994
+ })
995
+ }