most-box 0.2.1 → 0.2.2

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 (167) hide show
  1. package/README.md +66 -6
  2. package/electron/deepLink.js +29 -0
  3. package/electron/main.js +87 -20
  4. package/out/admin/index.html +0 -0
  5. package/out/app/index.html +0 -0
  6. package/out/assets/AppShell-BOtfY12t.js +1 -0
  7. package/out/assets/FilePreviewOverlay-C5qK9HAE.js +1 -0
  8. package/out/assets/LanguageToggle-Di5b88mK.js +1 -0
  9. package/out/assets/MarketingHeader-BOytKcCc.js +1 -0
  10. package/out/assets/MarketingLayout-B8m1Q7Pa.js +1 -0
  11. package/out/assets/{MarketingThemeToggle-qlwCZU1o.js → MarketingThemeToggle-C6fggkl7.js} +1 -1
  12. package/out/assets/{MilkdownEditor-_TGlDyA_.js → MilkdownEditor-Cfze75zl.js} +1 -1
  13. package/out/assets/OpenSidebarButton-DDuALgJ2.js +1 -0
  14. package/out/assets/SidebarAccount-bo1ypzrJ.js +1 -0
  15. package/out/assets/arrow-right-RldN7Rwi.js +1 -0
  16. package/out/assets/circle-alert-BQoSDxUe.js +1 -0
  17. package/out/assets/cloud-DHoTMeZY.js +1 -0
  18. package/out/assets/copy-BtJHJnqH.js +1 -0
  19. package/out/assets/download-Bg-OdoxM.js +1 -0
  20. package/out/assets/downloadValidation-CUvbvj9f.js +1 -0
  21. package/out/assets/external-link-m8ZIQe4p.js +1 -0
  22. package/out/assets/file-text-DG1orIkZ.js +1 -0
  23. package/out/assets/game-s6irY8hS.js +1 -0
  24. package/out/assets/hard-drive-BB-sllXA.js +1 -0
  25. package/out/assets/index-3OD3Chi9.css +1 -0
  26. package/out/assets/index-BSOvFG3o.css +1 -0
  27. package/out/assets/index-C0xqKeu-.css +1 -0
  28. package/out/assets/index-CrAXrmfP.js +31 -0
  29. package/out/assets/index.lazy-0Njp0U6I.js +1 -0
  30. package/out/assets/index.lazy-BTndBeBF.js +1 -0
  31. package/out/assets/index.lazy-Bq39jYTl.js +1 -0
  32. package/out/assets/index.lazy-Bu93oGzJ.js +2 -0
  33. package/out/assets/index.lazy-BvdBwgHx.js +1 -0
  34. package/out/assets/index.lazy-CIx8ist6.js +3 -0
  35. package/out/assets/index.lazy-DK7R297q.js +1 -0
  36. package/out/assets/index.lazy-DVOcrcEB.js +2 -0
  37. package/out/assets/index.lazy-DejOniAm.js +1 -0
  38. package/out/assets/index.lazy-DfsgyiMN.js +1 -0
  39. package/out/assets/index.lazy-DpNIiSXF.js +1 -0
  40. package/out/assets/index.lazy-Vnvz-t7T.js +1 -0
  41. package/out/assets/index.lazy-v7nBJ-sF.js +1 -0
  42. package/out/assets/key-round-DIQ3Xt5F.js +1 -0
  43. package/out/assets/lock-tf1t2yuy.js +1 -0
  44. package/out/assets/message-square-DaQH7q-P.js +1 -0
  45. package/out/assets/mp-DvFTsIL9.js +1 -0
  46. package/out/assets/music-DaFvU2DL.js +1 -0
  47. package/out/assets/notebook-pen-CqEFOHKx.js +1 -0
  48. package/out/assets/play-B7q6F75-.js +1 -0
  49. package/out/assets/plus-H2i2mspM.js +1 -0
  50. package/out/assets/refresh-cw-roxAhABl.js +1 -0
  51. package/out/assets/save-DR0O9ReR.js +1 -0
  52. package/out/assets/search-ncblG-zw.js +1 -0
  53. package/out/assets/send-m1XCcuPn.js +1 -0
  54. package/out/assets/shield-check-BwcvTU4U.js +1 -0
  55. package/out/assets/trash-2-CNpsqYc1.js +1 -0
  56. package/out/assets/triangle-alert-DP9EP7IM.js +1 -0
  57. package/out/assets/upload-V--8p13l.js +1 -0
  58. package/out/assets/useChannelMessages-46C52EyL.js +3 -0
  59. package/out/assets/useGameRoom-BtxPpfck.js +1 -0
  60. package/out/assets/useNavigate-DWlBD_-b.js +1 -0
  61. package/out/assets/userStore-C4vdYsQp.js +4 -0
  62. package/out/assets/wallet-CozFU6yK.js +1 -0
  63. package/out/assets/wifi-DIR3g_8A.js +1 -0
  64. package/out/chat/index.html +0 -0
  65. package/out/chat/join/index.html +0 -0
  66. package/out/download/index.html +2 -2
  67. package/out/game/gandengyan/index.html +0 -0
  68. package/out/game/index.html +0 -0
  69. package/out/game/zhajinhua/index.html +0 -0
  70. package/out/index.html +2 -2
  71. package/out/note/index.html +0 -0
  72. package/out/ping/index.html +2 -2
  73. package/out/profile/index.html +0 -0
  74. package/out/web3/index.html +0 -0
  75. package/package.json +9 -1
  76. package/server/index.js +36 -7
  77. package/server/src/core/channelIdentity.js +6 -0
  78. package/server/src/core/gameRoom.js +15 -5
  79. package/server/src/core/mostLink.js +8 -7
  80. package/server/src/core/zhajinhua.js +6 -0
  81. package/server/src/games/gandengyan.js +10 -0
  82. package/server/src/http/access.js +63 -12
  83. package/server/src/http/app.js +34 -838
  84. package/server/src/http/nodeStatus.js +101 -9
  85. package/server/src/http/routePolicy.js +2 -0
  86. package/server/src/http/routes/channelRoutes.js +163 -0
  87. package/server/src/http/routes/fileRoutes.js +345 -0
  88. package/server/src/http/routes/nodeRoutes.js +323 -0
  89. package/server/src/http/routes/seedRoutes.js +58 -0
  90. package/server/src/index.js +197 -16
  91. package/server/src/node/config.js +2 -6
  92. package/server/src/utils/avatar.js +30 -15
  93. package/server/src/utils/downloadMessages.js +0 -2
  94. package/out/assets/AppShell-OiOEqXPr.js +0 -1
  95. package/out/assets/ChatUi-Cif5LRF3.js +0 -1
  96. package/out/assets/CopyButton-Dm7krgbq.js +0 -1
  97. package/out/assets/LanguageToggle-B4ZNuBCV.js +0 -1
  98. package/out/assets/MarketingHeader-yIZuQP7m.js +0 -1
  99. package/out/assets/MarketingLayout-DVH0Nx7S.js +0 -1
  100. package/out/assets/MoveModal-BVr4Q7-b.js +0 -1
  101. package/out/assets/Nav-5xeettNJ.js +0 -1
  102. package/out/assets/NoteSidebar-DpniUKmy.js +0 -1
  103. package/out/assets/OpenSidebarButton-BfgG2HIT.js +0 -1
  104. package/out/assets/PemBlock-CxwIepth.js +0 -1
  105. package/out/assets/SidebarAccount-Zg5DZblE.js +0 -1
  106. package/out/assets/arrow-right-CL9YSDVS.js +0 -1
  107. package/out/assets/channelApi-DNdJfsJ-.js +0 -1
  108. package/out/assets/chevron-down-CnLh_-aO.js +0 -1
  109. package/out/assets/circle-alert-oiiRDvhx.js +0 -1
  110. package/out/assets/cloud-BEe2N89j.js +0 -1
  111. package/out/assets/code-9LB8QqxL.js +0 -1
  112. package/out/assets/copy-giX4rmFJ.js +0 -1
  113. package/out/assets/download-D0oMEYQZ.js +0 -1
  114. package/out/assets/downloadValidation-Bk1VsBBo.js +0 -1
  115. package/out/assets/external-link-Cm2WCUxv.js +0 -1
  116. package/out/assets/filePreview-BZ50vZZf.js +0 -1
  117. package/out/assets/game-Bvz4dspe.js +0 -1
  118. package/out/assets/hard-drive-B3CQbcp2.js +0 -1
  119. package/out/assets/index-8eWJAjpY.css +0 -1
  120. package/out/assets/index-BZc4blbW.css +0 -1
  121. package/out/assets/index-BkZvz4WA.css +0 -1
  122. package/out/assets/index-WCK14Vja.js +0 -34
  123. package/out/assets/index.lazy-5Q6GuMNT.js +0 -1
  124. package/out/assets/index.lazy-5jq6EFXa.js +0 -3
  125. package/out/assets/index.lazy-7n1Q-NrA.js +0 -3
  126. package/out/assets/index.lazy-BFnOyQFj.js +0 -1
  127. package/out/assets/index.lazy-B_oPp6qK.js +0 -1
  128. package/out/assets/index.lazy-BvY50KVz.js +0 -1
  129. package/out/assets/index.lazy-C0Kn_amZ.js +0 -1
  130. package/out/assets/index.lazy-C3cek3Gn.js +0 -1
  131. package/out/assets/index.lazy-CLpPkdy1.js +0 -1
  132. package/out/assets/index.lazy-Cpr1kApf.js +0 -2
  133. package/out/assets/index.lazy-CuwLZiUK.js +0 -1
  134. package/out/assets/index.lazy-DDc3Ylgf.js +0 -2
  135. package/out/assets/index.lazy-Dg3aqOss.js +0 -1
  136. package/out/assets/key-round-CzuljhND.js +0 -1
  137. package/out/assets/lock-D2NhNoJW.js +0 -1
  138. package/out/assets/message-square-DwBq_Go5.js +0 -1
  139. package/out/assets/mp-Bln2MB9G.js +0 -1
  140. package/out/assets/music-CB73K5Gz.js +0 -1
  141. package/out/assets/notebook-pen-Up7r5zoI.js +0 -1
  142. package/out/assets/play-OszVgROb.js +0 -1
  143. package/out/assets/plus-BbxQG_Ai.js +0 -1
  144. package/out/assets/save-CiqyiifY.js +0 -1
  145. package/out/assets/search-gqAPOsgS.js +0 -1
  146. package/out/assets/send-vwCWsZGP.js +0 -1
  147. package/out/assets/shield-check-CxWxsNLc.js +0 -1
  148. package/out/assets/trash-2-DNGr8IgF.js +0 -1
  149. package/out/assets/triangle-alert-B_1BlX1b.js +0 -1
  150. package/out/assets/upload-Dxl7GUzb.js +0 -1
  151. package/out/assets/useChannelMessages-7bYKXU_R.js +0 -3
  152. package/out/assets/useGameRoom-DqA1mkfk.js +0 -1
  153. package/out/assets/wallet-DlkawdPJ.js +0 -1
  154. package/out/assets/wifi-sBOKcPFM.js +0 -1
  155. package/out/demo/index.html +0 -0
  156. /package/out/avatars/default/{ocean.svg → dolphin.svg} +0 -0
  157. /package/out/avatars/default/{violet.svg → owl.svg} +0 -0
  158. /package/out/avatars/default/{mint.svg → panda.svg} +0 -0
  159. /package/out/avatars/default/{dusk.svg → snow-mountain.svg} +0 -0
  160. /package/out/avatars/default/{ember.svg → tiger.svg} +0 -0
  161. /package/out/avatars/default/{sage.svg → turtle.svg} +0 -0
  162. /package/public/avatars/default/{ocean.svg → dolphin.svg} +0 -0
  163. /package/public/avatars/default/{violet.svg → owl.svg} +0 -0
  164. /package/public/avatars/default/{mint.svg → panda.svg} +0 -0
  165. /package/public/avatars/default/{dusk.svg → snow-mountain.svg} +0 -0
  166. /package/public/avatars/default/{ember.svg → tiger.svg} +0 -0
  167. /package/public/avatars/default/{sage.svg → turtle.svg} +0 -0
@@ -2,14 +2,11 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { Hono } from 'hono'
4
4
  import { cors } from 'hono/cors'
5
- import { parseMostLink, validateCidString } from '../core/cid.js'
6
- import { sanitizeFilename } from '../utils/security.js'
7
- import { normalizeAddress, verifyAuthHeader } from '../utils/auth.js'
5
+ import { verifyAuthHeader } from '../utils/auth.js'
8
6
  import {
9
7
  DEFAULT_NODE_HOST,
10
8
  DEFAULT_NODE_PORT,
11
9
  createNodeConfigStore,
12
- evaluateStorageLimits,
13
10
  normalizeRemoteInvites,
14
11
  } from '../node/config.js'
15
12
  import { createNodeLogger } from '../node/logs.js'
@@ -20,29 +17,21 @@ import {
20
17
  hasValidInvite,
21
18
  isLocalRequest,
22
19
  isLocalUpgradeRequest,
23
- isLoopbackRemoteAddress,
24
20
  isPublicListenHost,
25
21
  isRemoteAccessRequest,
26
- remoteInviteConfigured,
27
22
  } from './access.js'
28
- import { badRequestOrAppError, errorJson } from './errors.js'
29
- import { resolveDataPathForSave } from './dataPath.js'
30
- import { listFilteredNodeLogs } from './nodeLogs.js'
31
- import {
32
- buildNodeStatus,
33
- buildOpenApiSpec,
34
- getNetworkAddresses,
35
- getPackageVersion,
36
- } from './nodeStatus.js'
23
+ import { buildNodeStatus } from './nodeStatus.js'
37
24
  import { createRateLimitMiddleware } from './rateLimit.js'
38
25
  import {
39
- validationErrorPayload,
40
26
  isPublicFileDownloadPath,
41
27
  requiresUserAuth,
42
28
  isAdminApi,
43
29
  } from './routePolicy.js'
44
- import { parseMultipartBusboy } from './uploads.js'
45
- import { getMimeType, registerStaticRoutes } from './staticFiles.js'
30
+ import { registerStaticRoutes } from './staticFiles.js'
31
+ import { registerChannelRoutes } from './routes/channelRoutes.js'
32
+ import { registerFileRoutes } from './routes/fileRoutes.js'
33
+ import { registerNodeRoutes } from './routes/nodeRoutes.js'
34
+ import { registerSeedRoutes } from './routes/seedRoutes.js'
46
35
 
47
36
  export { UPLOAD_TMP_DIR } from './uploads.js'
48
37
 
@@ -65,6 +54,8 @@ export function createApp(engine, options = {}) {
65
54
  options.nodeLogger || createNodeLogger(configStore.configDir || CONFIG_DIR)
66
55
  const wssRef = options.wssRef || { current: null }
67
56
  const serverInstanceRef = options.serverInstanceRef || { current: null }
57
+ const trustPrivateNetwork =
58
+ options.trustPrivateNetwork ?? isPublicListenHost(appHost)
68
59
  function getRemoteInviteSet() {
69
60
  const invites =
70
61
  options.remoteInvites === undefined
@@ -83,7 +74,7 @@ export function createApp(engine, options = {}) {
83
74
  invite: c.req.header('x-mostbox-invite'),
84
75
  origin: c.req.header('origin'),
85
76
  listenHost: appHost,
86
- local: isLocalRequest(c),
77
+ local: isLocalRequest(c, { trustPrivateNetwork }),
87
78
  })
88
79
  }
89
80
 
@@ -148,7 +139,12 @@ export function createApp(engine, options = {}) {
148
139
 
149
140
  async function broadcastNodeStatus() {
150
141
  try {
151
- const status = await buildNodeStatus(engine, configStore, appPort)
142
+ const status = await buildNodeStatus(
143
+ engine,
144
+ configStore,
145
+ appPort,
146
+ appHost
147
+ )
152
148
  wsBroadcast('node:status', status)
153
149
  return status
154
150
  } catch (err) {
@@ -220,7 +216,7 @@ export function createApp(engine, options = {}) {
220
216
  invite,
221
217
  origin: req.headers.origin,
222
218
  listenHost: appHost,
223
- local: isLocalUpgradeRequest(req),
219
+ local: isLocalUpgradeRequest(req, { trustPrivateNetwork }),
224
220
  })
225
221
  if (!remote) return true
226
222
 
@@ -283,824 +279,24 @@ export function createApp(engine, options = {}) {
283
279
  return c.json({ error: err.message, code: err.code }, 500)
284
280
  })
285
281
 
286
- // --- 配置路由 ---
287
- app.get('/api/node-id', c => {
288
- return c.json({ id: engine.getNodeId() })
289
- })
290
-
291
- app.get('/api/remote/capabilities', c => {
292
- const remoteInviteSet = getRemoteInviteSet()
293
- return c.json({
294
- remoteAccess:
295
- isPublicListenHost(appHost) && remoteInviteConfigured(remoteInviteSet),
296
- inviteRequired: true,
297
- inviteConfigured: remoteInviteConfigured(remoteInviteSet),
298
- authenticated: Boolean(c.get('userAddress')),
299
- userAddress: c.get('userAddress') || null,
300
- adminAvailable: !isRemoteRequest(c),
301
- listenHost: appHost,
302
- })
303
- })
304
-
305
- app.get('/api/config', c => {
306
- const config = configStore.loadRawConfig()
307
- return c.json({ dataPath: config.dataPath || '' })
308
- })
309
-
310
- app.post('/api/config', async c => {
311
- const body = await c.req.json()
312
- const patch = {}
313
-
314
- if (body.resetStorage) {
315
- patch.dataPath = ''
316
- } else if (body.dataPath !== undefined) {
317
- const resolved = resolveDataPathForSave(body.dataPath)
318
- if (resolved.error) return c.json({ error: resolved.error }, 400)
319
- patch.dataPath = resolved.dataPath
320
- }
321
-
322
- const { success } = configStore.saveNodeConfigPatch(patch)
323
- appendNodeLog({
324
- event: 'node:config:updated',
325
- message: 'Node config updated',
326
- data: { dataPath: getDataPath(configStore) },
327
- })
328
- await broadcastNodeStatus()
329
- return c.json({ success, dataPath: getDataPath(configStore) })
330
- })
331
-
332
- app.get('/api/config/data-path', c => {
333
- const config = configStore.getNodeConfig()
334
- const isDefault = !config.dataPath
335
- const dataPath = getDataPath(configStore)
336
- return c.json({ dataPath, isDefault })
337
- })
338
-
339
- app.get('/api/node/status', async c => {
340
- try {
341
- return c.json(await buildNodeStatus(engine, configStore, appPort))
342
- } catch (err) {
343
- return errorJson(c, err)
344
- }
345
- })
346
-
347
- app.get('/api/node/config', c => {
348
- const config = configStore.getNodeConfig()
349
- return c.json({
350
- ...config,
351
- dataPath: getDataPath(configStore),
352
- configuredDataPath: config.dataPath,
353
- isDefaultDataPath: !config.dataPath,
354
- currentHost: appHost,
355
- currentPort: appPort,
356
- remoteInvites: config.remoteInvites,
357
- })
358
- })
359
-
360
- app.post('/api/node/config', async c => {
361
- const body = await c.req.json()
362
- const patch = { ...body }
363
-
364
- if (body.resetStorage) {
365
- patch.dataPath = ''
366
- } else if (body.dataPath !== undefined) {
367
- const resolved = resolveDataPathForSave(body.dataPath)
368
- if (resolved.error) return c.json({ error: resolved.error }, 400)
369
- patch.dataPath = resolved.dataPath
370
- }
371
-
372
- const { success, config } = configStore.saveNodeConfigPatch(patch)
373
- engine.setMaxFileSize(config.maxFileSizeBytes)
374
- appendNodeLog({
375
- event: 'node:config:updated',
376
- message: 'Node daemon config updated',
377
- data: {
378
- dataPath: getDataPath(configStore),
379
- port: config.port,
380
- capacityBytes: config.capacityBytes,
381
- remoteInviteCount: config.remoteInvites.length,
382
- },
383
- })
384
- await broadcastNodeStatus()
385
- return c.json({ success, ...config, dataPath: getDataPath(configStore) })
386
- })
387
-
388
- app.get('/api/node/policy', c => {
389
- const config = configStore.getNodeConfig()
390
- return c.json({
391
- maxFileSizeBytes: config.maxFileSizeBytes,
392
- })
393
- })
394
-
395
- app.post('/api/node/policy', async c => {
396
- const body = await c.req.json()
397
- const { success, config } = configStore.saveNodeConfigPatch({
398
- maxFileSizeBytes: body.maxFileSizeBytes,
399
- })
400
- engine.setMaxFileSize(config.maxFileSizeBytes)
401
- const policy = {
402
- maxFileSizeBytes: config.maxFileSizeBytes,
403
- }
404
- appendNodeLog({
405
- event: 'node:policy:updated',
406
- message: 'Node storage limits updated',
407
- data: policy,
408
- })
409
- await broadcastNodeStatus()
410
- return c.json({ success, ...policy })
411
- })
412
-
413
- app.post('/api/node/policy/evaluate', async c => {
414
- const body = await c.req.json()
415
- const decision = evaluateStorageLimits(configStore.getNodeConfig(), body)
416
- return c.json(decision)
417
- })
418
-
419
- app.get('/api/node/logs', c => {
420
- const limit = Number(c.req.query('limit') || 100)
421
- const filter = c.req.query('filter') || 'all'
422
- const query = c.req.query('q') || ''
423
- const result = listFilteredNodeLogs(nodeLogger, { limit, filter, query })
424
- return c.json({
425
- logFile: nodeLogger.logFile,
426
- filter: result.filter,
427
- query: result.query,
428
- logs: result.logs,
429
- })
430
- })
431
-
432
- app.delete('/api/node/logs', c => {
433
- const success = nodeLogger.clear()
434
- const clearedAt = new Date().toISOString()
435
- wsBroadcast('node:logs:cleared', { clearedAt })
436
- return c.json({ success, clearedAt })
437
- })
438
-
439
- app.get('/api/node/diagnostics', async c => {
440
- try {
441
- const status = await buildNodeStatus(engine, configStore, appPort)
442
- return c.json({
443
- generatedAt: new Date().toISOString(),
444
- packageVersion: getPackageVersion(),
445
- platform: process.platform,
446
- nodeVersion: process.version,
447
- status,
448
- logFile: nodeLogger.logFile,
449
- logs: nodeLogger.list(200),
450
- })
451
- } catch (err) {
452
- return errorJson(c, err)
453
- }
454
- })
455
-
456
- app.get('/api/admin/users', c => {
457
- return c.json({ users: engine.listUsers() })
458
- })
459
-
460
- app.post('/api/user/sync/start', async c => {
461
- try {
462
- const body = await c.req.json()
463
- const result = await engine.startUserSync(c.get('userAddress'), body)
464
- appendNodeLog({
465
- event: 'node:user-sync:started',
466
- message: 'User sync started',
467
- data: {
468
- ownerAddress: result.ownerAddress,
469
- syncName: result.syncName,
470
- },
471
- })
472
- return c.json({ success: true, ...result })
473
- } catch (err) {
474
- return errorJson(c, err)
475
- }
476
- })
477
-
478
- app.get('/api/user/sync/status', c => {
479
- try {
480
- return c.json(engine.getUserSyncStatus(c.get('userAddress')))
481
- } catch (err) {
482
- return errorJson(c, err)
483
- }
484
- })
485
-
486
- app.delete('/api/admin/users/:address/data', async c => {
487
- const address = normalizeAddress(c.req.param('address'))
488
- if (!address) {
489
- return c.json({ error: 'valid address is required' }, 400)
490
- }
491
- try {
492
- const result = await engine.clearUserData(address)
493
- appendNodeLog({
494
- event: 'node:user-data:cleared',
495
- message: 'User data cleared',
496
- data: result,
497
- })
498
- await broadcastNodeStatus()
499
- return c.json({ success: true, ...result })
500
- } catch (err) {
501
- return errorJson(c, err)
502
- }
503
- })
504
-
505
- app.get('/api/openapi.json', c => {
506
- return c.json(buildOpenApiSpec(appPort))
507
- })
508
-
509
- // --- 网络路由 ---
510
- app.get('/api/network-status', c => {
511
- return c.json(engine.getNetworkStatus())
512
- })
513
-
514
- app.get('/api/network', c => {
515
- return c.json(getNetworkAddresses(appPort))
516
- })
517
-
518
- // --- 节点保种路由 ---
519
- app.get('/api/node/holdings', c => {
520
- try {
521
- return c.json(engine.listHoldings())
522
- } catch (err) {
523
- return errorJson(c, err)
524
- }
525
- })
526
-
527
- app.post('/api/node/holdings', async c => {
528
- try {
529
- const body = await c.req.json()
530
- const holding = await engine.addHolding(body)
531
- appendNodeLog({
532
- event: 'node:holding:added',
533
- message: 'Node holding added',
534
- data: { cid: holding.cid, size: holding.size },
535
- })
536
- await broadcastNodeStatus()
537
- return c.json({ success: true, holding })
538
- } catch (err) {
539
- return errorJson(c, err)
540
- }
541
- })
542
-
543
- app.post('/api/p2p/pull', async c => {
544
- try {
545
- const body = await c.req.json()
546
- const timeout =
547
- body.timeout === undefined ? undefined : Number(body.timeout)
548
- const result = await engine.pullByCid({
549
- ...body,
550
- timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
551
- })
552
- appendNodeLog({
553
- event: 'node:pull:success',
554
- message: 'P2P pull completed',
555
- data: { cid: result.cid, taskId: result.taskId },
556
- })
557
- await broadcastNodeStatus()
558
- return c.json({ success: true, ...result })
559
- } catch (err) {
560
- appendNodeLog({
561
- level: 'error',
562
- event: 'node:pull:error',
563
- message: err.message,
564
- data: { code: err.code || 'UNKNOWN' },
565
- })
566
- return errorJson(c, err)
567
- }
568
- })
569
-
570
- // --- 文件路由 ---
571
- app.get('/api/files', c => {
572
- return c.json(
573
- engine.listPublishedFiles({ ownerAddress: c.get('userAddress') })
574
- )
575
- })
576
-
577
- app.post('/api/publish', async c => {
578
- const req = c.env.incoming
579
- const result = await parseMultipartBusboy(
580
- req,
581
- configStore.getNodeConfig().maxFileSizeBytes
582
- )
583
-
584
- if (!result || !result.filename) {
585
- return c.json({ error: 'No file provided' }, 400)
586
- }
587
-
588
- try {
589
- const publishResult = await engine.publishFile(
590
- result.filePath,
591
- result.filename,
592
- { ownerAddress: c.get('userAddress') }
593
- )
594
- return c.json({ success: true, ...publishResult })
595
- } finally {
596
- fs.unlink(result.filePath, () => {})
597
- }
598
- })
599
-
600
- app.post('/api/download/check', async c => {
601
- const body = await c.req.json()
602
- if (!body.link) {
603
- return c.json({ error: 'link is required' }, 400)
604
- }
605
-
606
- const parsed = parseMostLink(body.link)
607
- if (parsed.errorCode) {
608
- return c.json(
609
- validationErrorPayload(parsed.errorCode, parsed.details),
610
- 400
611
- )
612
- }
613
-
614
- const localAvailability = await engine.getLocalCidAvailability(body.link, {
615
- ownerAddress: c.get('userAddress'),
616
- })
617
- if (localAvailability) {
618
- return c.json({
619
- success: true,
620
- available: true,
621
- cid: parsed.cid,
622
- fileName: localAvailability.fileName,
623
- size: Number(localAvailability.size) || null,
624
- alreadyExists: true,
625
- })
626
- }
627
-
628
- if (engine.hasDownloadNameConflict(parsed.fileName)) {
629
- return c.json(
630
- {
631
- error: `已有同名文件: ${parsed.fileName}`,
632
- code: 'CONFLICT',
633
- },
634
- 409
635
- )
636
- }
637
-
638
- try {
639
- const timeout =
640
- body.timeout === undefined ? undefined : Number(body.timeout)
641
- const result = await engine.checkDownloadAvailability(body.link, {
642
- ownerAddress: c.get('userAddress'),
643
- timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
644
- })
645
- return c.json({ success: true, ...result })
646
- } catch (err) {
647
- return errorJson(c, err)
648
- }
649
- })
650
-
651
- app.post('/api/download', async c => {
652
- const body = await c.req.json()
653
- if (!body.link) {
654
- return c.json({ error: 'link is required' }, 400)
655
- }
656
-
657
- const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
658
-
659
- const parsed = parseMostLink(body.link)
660
- if (parsed.errorCode) {
661
- return c.json(
662
- validationErrorPayload(parsed.errorCode, parsed.details),
663
- 400
664
- )
665
- }
666
-
667
- const localAvailability = await engine.getLocalCidAvailability(body.link, {
668
- ownerAddress: c.get('userAddress'),
669
- })
670
- if (localAvailability) {
671
- console.log(
672
- `[MostBox] CID content already exists locally: ${parsed.cid}`
673
- )
674
- try {
675
- const result = await engine.downloadFile(body.link, taskId, {
676
- ownerAddress: c.get('userAddress'),
677
- })
678
- return c.json({ success: true, ...result })
679
- } catch (err) {
680
- return errorJson(c, err)
681
- }
682
- }
683
-
684
- if (engine.hasDownloadNameConflict(parsed.fileName)) {
685
- return c.json(
686
- {
687
- error: `已有同名文件: ${parsed.fileName}`,
688
- code: 'CONFLICT',
689
- },
690
- 409
691
- )
692
- }
693
-
694
- engine
695
- .downloadFile(body.link, taskId, { ownerAddress: c.get('userAddress') })
696
- .catch(err => {
697
- if (err.message === 'Download cancelled') {
698
- wsBroadcast('download:cancelled', { taskId })
699
- } else {
700
- wsBroadcast('download:error', { taskId, error: err.message })
701
- }
702
- })
703
-
704
- return c.json({ success: true, taskId })
705
- })
706
-
707
- app.post('/api/download/cancel', async c => {
708
- const body = await c.req.json()
709
- if (!body.taskId) {
710
- return c.json({ error: 'taskId is required' }, 400)
711
- }
712
- engine.cancelDownload(body.taskId)
713
- return c.json({ success: true })
714
- })
715
-
716
- app.delete('/api/files/:cid', async c => {
717
- const cid = c.req.param('cid')
718
- const cidValidation = validateCidString(cid)
719
- if (!cidValidation.valid) {
720
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
721
- }
722
- const result = await engine.deletePublishedFile(cid, {
723
- ownerAddress: c.get('userAddress'),
724
- })
725
- return c.json(result)
726
- })
727
-
728
- app.post('/api/files/:cid/cache', async c => {
729
- const cid = c.req.param('cid')
730
- const cidValidation = validateCidString(cid)
731
- if (!cidValidation.valid) {
732
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
733
- }
734
- try {
735
- const body = await c.req.json().catch(() => ({}))
736
- const timeout =
737
- body.timeout === undefined ? undefined : Number(body.timeout)
738
- const result = await engine.cacheFile(cid, {
739
- ownerAddress: c.get('userAddress'),
740
- timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
741
- taskId: body.taskId,
742
- })
743
- return c.json({ success: true, ...result })
744
- } catch (err) {
745
- return badRequestOrAppError(c, err)
746
- }
747
- })
748
-
749
- app.post('/api/move', async c => {
750
- const body = await c.req.json()
751
- if (!body.cid || !body.newFileName) {
752
- return c.json({ error: 'cid and newFileName are required' }, 400)
753
- }
754
- const cidValidation = validateCidString(body.cid)
755
- if (!cidValidation.valid) {
756
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
757
- }
758
- const cleanFileName = sanitizeFilename(body.newFileName)
759
- if (
760
- !cleanFileName ||
761
- cleanFileName === 'unnamed' ||
762
- body.newFileName.length > 255
763
- ) {
764
- return c.json({ error: 'Invalid filename' }, 400)
765
- }
766
- try {
767
- const result = engine.moveFile(body.cid, cleanFileName, {
768
- ownerAddress: c.get('userAddress'),
769
- })
770
- return c.json({ success: true, ...result })
771
- } catch (err) {
772
- return badRequestOrAppError(c, err)
773
- }
774
- })
775
-
776
- app.get('/api/files/:cid/download', async c => {
777
- const cid = c.req.param('cid')
778
- const cidValidation = validateCidString(cid)
779
- if (!cidValidation.valid) {
780
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
781
- }
782
-
783
- const rangeHeader = c.req.header('range')
784
-
785
- try {
786
- if (rangeHeader) {
787
- const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
788
- if (rangeMatch) {
789
- const start = parseInt(rangeMatch[1], 10)
790
- const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
791
- const offset = start
792
- const limit = end !== undefined ? end - start + 1 : undefined
793
-
794
- const result = await engine.readFileRaw(cid, {
795
- offset,
796
- limit,
797
- public: true,
798
- })
799
- const contentType = getMimeType(result.fileName)
800
-
801
- c.header('Content-Type', contentType)
802
- c.header('Content-Length', String(result.buffer.length))
803
- c.header(
804
- 'Content-Range',
805
- `bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`
806
- )
807
- c.header('Accept-Ranges', 'bytes')
808
- c.status(206)
809
- return c.body(result.buffer)
810
- }
811
- }
812
-
813
- const result = await engine.readFileRaw(cid, {
814
- public: true,
815
- })
816
- const contentType = getMimeType(result.fileName)
817
- c.header('Content-Type', contentType)
818
- c.header('Content-Length', String(result.totalSize))
819
- c.header('Accept-Ranges', 'bytes')
820
- c.header(
821
- 'Content-Disposition',
822
- `inline; filename="${encodeURIComponent(result.fileName)}"`
823
- )
824
- return c.body(result.buffer)
825
- } catch (err) {
826
- if (err.message === 'File not found') {
827
- return c.json({ error: err.message }, 404)
828
- }
829
- return c.json({ error: err.message }, 400)
830
- }
831
- })
832
-
833
- // --- 回收站路由 ---
834
- app.get('/api/trash', c => {
835
- return c.json(engine.listTrashFiles({ ownerAddress: c.get('userAddress') }))
836
- })
837
-
838
- app.post('/api/trash/:cid/restore', async c => {
839
- const cid = c.req.param('cid')
840
- const cidValidation = validateCidString(cid)
841
- if (!cidValidation.valid) {
842
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
843
- }
844
- try {
845
- const result = await engine.restoreTrashFile(cid, {
846
- ownerAddress: c.get('userAddress'),
847
- })
848
- return c.json({ success: true, files: result })
849
- } catch (err) {
850
- return c.json({ error: err.message }, 400)
851
- }
852
- })
853
-
854
- app.delete('/api/trash/:cid', async c => {
855
- const cid = c.req.param('cid')
856
- const cidValidation = validateCidString(cid)
857
- if (!cidValidation.valid) {
858
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
859
- }
860
- const result = await engine.permanentDeleteTrashFile(cid, {
861
- ownerAddress: c.get('userAddress'),
862
- })
863
- return c.json({ success: true, trashFiles: result })
864
- })
865
-
866
- app.delete('/api/trash', async c => {
867
- const result = await engine.emptyTrash({
868
- ownerAddress: c.get('userAddress'),
869
- })
870
- return c.json({ success: true, trashFiles: result })
871
- })
872
-
873
- // --- 收藏路由 ---
874
- app.post('/api/files/:cid/star', async c => {
875
- const cid = c.req.param('cid')
876
- const cidValidation = validateCidString(cid)
877
- if (!cidValidation.valid) {
878
- return c.json(validationErrorPayload(cidValidation.errorCode), 400)
879
- }
880
- try {
881
- const result = engine.toggleStarred(cid, {
882
- ownerAddress: c.get('userAddress'),
883
- })
884
- return c.json({ success: true, ...result })
885
- } catch (err) {
886
- return c.json({ error: err.message }, 400)
887
- }
888
- })
889
-
890
- // --- 显示名路由 ---
891
- app.get('/api/display-name', c => {
892
- return c.json({ displayName: engine.getDisplayName() })
893
- })
894
-
895
- app.post('/api/display-name', async c => {
896
- const body = await c.req.json()
897
- if (!body.name || !body.name.trim()) {
898
- return c.json({ error: 'name is required' }, 400)
899
- }
900
- const trimmed = body.name.trim()
901
- if (trimmed.length > 100) {
902
- return c.json({ error: 'Name too long (max 100 chars)' }, 400)
903
- }
904
- if (/[<>]/.test(trimmed)) {
905
- return c.json({ error: 'Name contains invalid characters' }, 400)
906
- }
907
- const success = engine.setDisplayName(trimmed)
908
- return c.json({ success, displayName: engine.getDisplayName() })
909
- })
910
-
911
- // --- 频道路由 ---
912
- app.post('/api/channels', async c => {
913
- const body = await c.req.json()
914
- if (!body.name || !body.name.trim()) {
915
- return c.json({ error: 'name is required' }, 400)
916
- }
917
- try {
918
- const result = await engine.createChannel(
919
- body.name.trim(),
920
- body.type || 'personal',
921
- {
922
- ownerAddress: c.get('userAddress'),
923
- displayName: body.displayName,
924
- avatar: body.avatar,
925
- discover: true,
926
- }
927
- )
928
- return c.json({ success: true, ...result })
929
- } catch (err) {
930
- return c.json({ error: err.message }, 400)
931
- }
932
- })
933
-
934
- app.get('/api/channels', c => {
935
- return c.json(
936
- engine.listChannels({
937
- ownerAddress: c.get('userAddress'),
938
- type: c.req.query('type'),
939
- excludeType: c.req.query('excludeType'),
940
- })
941
- )
942
- })
943
-
944
- const leaveChannelForRequest = async (c, channelIdentifier) => {
945
- const name = String(channelIdentifier || '').trim()
946
- if (!name) {
947
- return c.json({ error: '频道标识不能为空' }, 400)
948
- }
949
- try {
950
- const result = await engine.leaveChannel(name, {
951
- ownerAddress: c.get('userAddress'),
952
- })
953
- return c.json({ success: true, channels: result })
954
- } catch (err) {
955
- return c.json({ error: err.message }, 400)
956
- }
957
- }
958
-
959
- app.delete('/api/channels', async c => {
960
- const body = await c.req.json().catch(() => ({}))
961
- return leaveChannelForRequest(c, body.channelKey || body.name)
962
- })
963
-
964
- app.get('/api/channels/:name/messages', async c => {
965
- const name = c.req.param('name')
966
- const limit = parseInt(c.req.query('limit') || '100', 10)
967
- const offset = parseInt(c.req.query('offset') || '0', 10)
968
- try {
969
- const messages = await engine.getChannelMessages(name, {
970
- limit,
971
- offset,
972
- ownerAddress: c.get('userAddress'),
973
- })
974
- return c.json(messages)
975
- } catch (err) {
976
- return badRequestOrAppError(c, err)
977
- }
978
- })
979
-
980
- app.get('/api/channels/:name/members', c => {
981
- try {
982
- return c.json(
983
- engine.getChannelMembers(c.req.param('name'), {
984
- ownerAddress: c.get('userAddress'),
985
- })
986
- )
987
- } catch (err) {
988
- return badRequestOrAppError(c, err)
989
- }
990
- })
991
-
992
- app.post('/api/channels/:name/messages', async c => {
993
- const name = c.req.param('name')
994
- const body = await c.req.json()
995
- if (!body.content || !body.content.trim()) {
996
- return c.json({ error: 'content is required' }, 400)
997
- }
998
- if (!body.author || !body.authorName) {
999
- return c.json({ error: 'author and authorName are required' }, 400)
1000
- }
1001
- if (!/^0x[a-fA-F0-9]{40}$/.test(body.author)) {
1002
- return c.json({ error: 'Invalid author format' }, 400)
1003
- }
1004
- if (normalizeAddress(body.author) !== c.get('userAddress')) {
1005
- return c.json({ error: 'message author must match logged-in user' }, 403)
1006
- }
1007
- if (body.authorName.length > 50) {
1008
- return c.json({ error: 'authorName too long' }, 400)
1009
- }
1010
- try {
1011
- const message = await engine.sendMessage(
1012
- name,
1013
- body.content,
1014
- body.author,
1015
- body.authorName,
1016
- {
1017
- ownerAddress: c.get('userAddress'),
1018
- attachment: body.attachment,
1019
- avatar: body.avatar,
1020
- }
1021
- )
1022
- return c.json({ success: true, message })
1023
- } catch (err) {
1024
- return badRequestOrAppError(c, err)
1025
- }
1026
- })
1027
-
1028
- app.get('/api/channels/:name/peers', c => {
1029
- try {
1030
- return c.json(
1031
- engine.getChannelPeers(c.req.param('name'), {
1032
- ownerAddress: c.get('userAddress'),
1033
- })
1034
- )
1035
- } catch (err) {
1036
- return badRequestOrAppError(c, err)
1037
- }
1038
- })
1039
-
1040
- app.put('/api/channels/:name/remark', async c => {
1041
- const name = c.req.param('name')
1042
- const body = await c.req.json()
1043
- try {
1044
- const remark = engine.setChannelRemark(name, body.remark, {
1045
- ownerAddress: c.get('userAddress'),
1046
- })
1047
- return c.json({ success: true, remark })
1048
- } catch (err) {
1049
- return c.json({ error: err.message }, 400)
1050
- }
1051
- })
1052
-
1053
- app.put('/api/channels/:name/pin', async c => {
1054
- const name = c.req.param('name')
1055
- const body = await c.req.json()
1056
- try {
1057
- const pinned = engine.setChannelPinned(name, Boolean(body.pinned), {
1058
- ownerAddress: c.get('userAddress'),
1059
- })
1060
- return c.json({ success: true, pinned })
1061
- } catch (err) {
1062
- return c.json({ error: err.message }, 400)
1063
- }
1064
- })
1065
-
1066
- // --- 文件夹重命名 ---
1067
- app.post('/api/folder/rename', async c => {
1068
- const body = await c.req.json()
1069
- if (!body.oldPath || !body.newPath) {
1070
- return c.json({ error: 'oldPath and newPath are required' }, 400)
1071
- }
1072
- if (body.oldPath.length > 500 || body.newPath.length > 500) {
1073
- return c.json({ error: 'Path too long' }, 400)
1074
- }
1075
- if (body.oldPath.includes('..') || body.newPath.includes('..')) {
1076
- return c.json({ error: 'Path traversal not allowed' }, 400)
1077
- }
1078
- try {
1079
- const result = engine.renameFolder(body.oldPath, body.newPath, {
1080
- ownerAddress: c.get('userAddress'),
1081
- })
1082
- return c.json({ success: true, ...result })
1083
- } catch (err) {
1084
- return badRequestOrAppError(c, err)
1085
- }
1086
- })
1087
-
1088
- // --- 关机路由 ---
1089
- app.post('/api/shutdown', c => {
1090
- const clientIp = c.env.incoming?.socket?.remoteAddress || 'unknown'
1091
- if (!isLoopbackRemoteAddress(clientIp)) {
1092
- return c.json({ error: 'Forbidden' }, 403)
1093
- }
1094
- c.json({ success: true })
1095
- console.log('[MostBox] Shutdown requested via API...')
1096
- setTimeout(async () => {
1097
- await engine.stop()
1098
- if (serverInstanceRef.current) serverInstanceRef.current.close()
1099
- console.log('[MostBox] Server stopped.')
1100
- process.exit(0)
1101
- }, 100)
1102
- return c.body(null)
282
+ // --- API 路由注册 ---
283
+ registerNodeRoutes(app, {
284
+ engine,
285
+ appPort,
286
+ appHost,
287
+ configStore,
288
+ nodeLogger,
289
+ getDataPath,
290
+ getRemoteInviteSet,
291
+ isRemoteRequest,
292
+ appendNodeLog,
293
+ broadcastNodeStatus,
294
+ wsBroadcast,
295
+ serverInstanceRef,
1103
296
  })
297
+ registerSeedRoutes(app, { engine, appendNodeLog, broadcastNodeStatus })
298
+ registerFileRoutes(app, { engine, configStore, wsBroadcast })
299
+ registerChannelRoutes(app, { engine })
1104
300
 
1105
301
  registerStaticRoutes(app)
1106
302