most-box 0.1.1 → 0.1.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 (215) hide show
  1. package/README.md +21 -14
  2. package/electron/main.js +72 -8
  3. package/electron/preload.js +6 -0
  4. package/out/404/index.html +2 -2
  5. package/out/404.html +2 -2
  6. package/out/__next.__PAGE__.txt +6 -6
  7. package/out/__next._full.txt +16 -16
  8. package/out/__next._head.txt +3 -3
  9. package/out/__next._index.txt +8 -8
  10. package/out/__next._tree.txt +5 -5
  11. package/out/_next/static/chunks/0.e2avjgna_b2.js +1 -0
  12. package/out/_next/static/chunks/{0ho~log~~-jwp.css → 03h~nhgj0hv3p.css} +1 -1
  13. package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
  14. package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
  15. package/out/_next/static/chunks/0aq.rc9woa2nz.js +1 -0
  16. package/out/_next/static/chunks/0etes81d_cihn.js +1 -0
  17. package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
  18. package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
  19. package/out/_next/static/chunks/0gwian.hp3-92.js +1 -0
  20. package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
  21. package/out/_next/static/chunks/0l5_.uqb-uqb8.js +1 -0
  22. package/out/_next/static/chunks/0mex8svsiv-2l.js +1 -0
  23. package/out/_next/static/chunks/0myq9gs8szydh.js +1 -0
  24. package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
  25. package/out/_next/static/chunks/0p0sv~fuddvgr.js +1 -0
  26. package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
  27. package/out/_next/static/chunks/0q0ksgxg98xgd.js +1 -0
  28. package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
  29. package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
  30. package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
  31. package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
  32. package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
  33. package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
  34. package/out/_next/static/chunks/14_inksek_rth.js +2 -0
  35. package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
  36. package/out/_next/static/chunks/16xls5tt_68lx.js +1 -0
  37. package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
  38. package/out/_not-found/__next._full.txt +13 -13
  39. package/out/_not-found/__next._head.txt +3 -3
  40. package/out/_not-found/__next._index.txt +8 -8
  41. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  42. package/out/_not-found/__next._not-found.txt +3 -3
  43. package/out/_not-found/__next._tree.txt +3 -3
  44. package/out/_not-found/index.html +2 -2
  45. package/out/_not-found/index.txt +13 -13
  46. package/out/admin/__next._full.txt +15 -15
  47. package/out/admin/__next._head.txt +3 -3
  48. package/out/admin/__next._index.txt +8 -8
  49. package/out/admin/__next._tree.txt +4 -4
  50. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  51. package/out/admin/__next.admin.txt +4 -4
  52. package/out/admin/index.html +2 -2
  53. package/out/admin/index.txt +15 -15
  54. package/out/app/__next._full.txt +14 -14
  55. package/out/app/__next._head.txt +3 -3
  56. package/out/app/__next._index.txt +8 -8
  57. package/out/app/__next._tree.txt +3 -3
  58. package/out/app/__next.app.__PAGE__.txt +4 -4
  59. package/out/app/__next.app.txt +3 -3
  60. package/out/app/index.html +2 -2
  61. package/out/app/index.txt +14 -14
  62. package/out/chat/__next._full.txt +15 -15
  63. package/out/chat/__next._head.txt +3 -3
  64. package/out/chat/__next._index.txt +8 -8
  65. package/out/chat/__next._tree.txt +4 -4
  66. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  67. package/out/chat/__next.chat.txt +4 -4
  68. package/out/chat/index.html +2 -2
  69. package/out/chat/index.txt +15 -15
  70. package/out/chat/join/__next._full.txt +25 -0
  71. package/out/chat/join/__next._head.txt +5 -0
  72. package/out/{changelog → chat/join}/__next._index.txt +8 -8
  73. package/out/chat/join/__next._tree.txt +5 -0
  74. package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
  75. package/out/chat/join/__next.chat.join.txt +5 -0
  76. package/out/chat/join/__next.chat.txt +5 -0
  77. package/out/chat/join/index.html +15 -0
  78. package/out/chat/join/index.txt +25 -0
  79. package/out/download/__next._full.txt +21 -21
  80. package/out/download/__next._head.txt +3 -3
  81. package/out/download/__next._index.txt +8 -8
  82. package/out/download/__next._tree.txt +5 -5
  83. package/out/download/__next.download.__PAGE__.txt +9 -9
  84. package/out/download/__next.download.txt +3 -3
  85. package/out/download/index.html +2 -2
  86. package/out/download/index.txt +21 -21
  87. package/out/index.html +2 -2
  88. package/out/index.txt +16 -16
  89. package/out/note/__next._full.txt +14 -14
  90. package/out/note/__next._head.txt +3 -3
  91. package/out/note/__next._index.txt +8 -8
  92. package/out/note/__next._tree.txt +3 -3
  93. package/out/note/__next.note.__PAGE__.txt +4 -4
  94. package/out/note/__next.note.txt +3 -3
  95. package/out/note/index.html +2 -2
  96. package/out/note/index.txt +14 -14
  97. package/out/ping/__next._full.txt +16 -16
  98. package/out/ping/__next._head.txt +3 -3
  99. package/out/ping/__next._index.txt +8 -8
  100. package/out/ping/__next._tree.txt +5 -5
  101. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  102. package/out/ping/__next.ping.txt +4 -4
  103. package/out/ping/index.html +2 -2
  104. package/out/ping/index.txt +16 -16
  105. package/out/web3/__next._full.txt +15 -15
  106. package/out/web3/__next._head.txt +3 -3
  107. package/out/web3/__next._index.txt +8 -8
  108. package/out/web3/__next._tree.txt +4 -4
  109. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  110. package/out/web3/__next.web3.txt +4 -4
  111. package/out/web3/ed25519/__next._full.txt +13 -13
  112. package/out/web3/ed25519/__next._head.txt +3 -3
  113. package/out/web3/ed25519/__next._index.txt +8 -8
  114. package/out/web3/ed25519/__next._tree.txt +4 -4
  115. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  116. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  117. package/out/web3/ed25519/__next.web3.txt +4 -4
  118. package/out/web3/ed25519/index.html +1 -1
  119. package/out/web3/ed25519/index.txt +13 -13
  120. package/out/web3/index.html +2 -2
  121. package/out/web3/index.txt +15 -15
  122. package/out/web3/tools/__next._full.txt +13 -13
  123. package/out/web3/tools/__next._head.txt +3 -3
  124. package/out/web3/tools/__next._index.txt +8 -8
  125. package/out/web3/tools/__next._tree.txt +4 -4
  126. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  127. package/out/web3/tools/__next.web3.tools.txt +3 -3
  128. package/out/web3/tools/__next.web3.txt +4 -4
  129. package/out/web3/tools/index.html +1 -1
  130. package/out/web3/tools/index.txt +13 -13
  131. package/package.json +19 -9
  132. package/server/index.js +133 -1303
  133. package/server/src/config.js +1 -1
  134. package/server/src/core/channelAttachment.js +68 -0
  135. package/server/src/core/cid.js +2 -88
  136. package/server/src/core/cidTopic.js +29 -0
  137. package/server/src/core/mostLink.js +88 -0
  138. package/server/src/http/access.js +123 -0
  139. package/server/src/http/app.js +1095 -0
  140. package/server/src/http/errors.js +35 -0
  141. package/server/src/http/nodeLogs.js +53 -0
  142. package/server/src/http/nodeStatus.js +146 -0
  143. package/server/src/http/staticFiles.js +84 -0
  144. package/server/src/http/uploads.js +114 -0
  145. package/server/src/index.js +799 -211
  146. package/server/src/node/config.js +38 -6
  147. package/server/src/utils/api.js +287 -14
  148. package/server/src/utils/auth.js +63 -0
  149. package/server/src/utils/dateTime.js +30 -0
  150. package/server/src/utils/downloadMessages.js +89 -0
  151. package/server/src/utils/errors.js +7 -0
  152. package/server/src/utils/mostWallet.js +151 -0
  153. package/server/src/utils/mp.js +2 -26
  154. package/server/src/utils/noteBackup.js +2 -5
  155. package/server/src/utils/noteUtils.js +11 -3
  156. package/server/src/utils/userIdentity.js +0 -1
  157. package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
  158. package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
  159. package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
  160. package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
  161. package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
  162. package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
  163. package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
  164. package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
  165. package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
  166. package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
  167. package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
  168. package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
  169. package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
  170. package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
  171. package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
  172. package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
  173. package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
  174. package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
  175. package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
  176. package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
  177. package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
  178. package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
  179. package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
  180. package/out/changelog/__next._full.txt +0 -25
  181. package/out/changelog/__next._head.txt +0 -5
  182. package/out/changelog/__next._tree.txt +0 -5
  183. package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
  184. package/out/changelog/__next.changelog.txt +0 -5
  185. package/out/changelog/index.html +0 -15
  186. package/out/changelog/index.txt +0 -25
  187. package/out/docs/__next._full.txt +0 -25
  188. package/out/docs/__next._head.txt +0 -5
  189. package/out/docs/__next._index.txt +0 -9
  190. package/out/docs/__next._tree.txt +0 -5
  191. package/out/docs/__next.docs.__PAGE__.txt +0 -10
  192. package/out/docs/__next.docs.txt +0 -5
  193. package/out/docs/getting-started/__next._full.txt +0 -25
  194. package/out/docs/getting-started/__next._head.txt +0 -5
  195. package/out/docs/getting-started/__next._index.txt +0 -9
  196. package/out/docs/getting-started/__next._tree.txt +0 -5
  197. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
  198. package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
  199. package/out/docs/getting-started/__next.docs.txt +0 -5
  200. package/out/docs/getting-started/index.html +0 -15
  201. package/out/docs/getting-started/index.txt +0 -25
  202. package/out/docs/index.html +0 -15
  203. package/out/docs/index.txt +0 -25
  204. package/out/note/edit/__next._full.txt +0 -24
  205. package/out/note/edit/__next._head.txt +0 -5
  206. package/out/note/edit/__next._index.txt +0 -9
  207. package/out/note/edit/__next._tree.txt +0 -4
  208. package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
  209. package/out/note/edit/__next.note.edit.txt +0 -5
  210. package/out/note/edit/__next.note.txt +0 -5
  211. package/out/note/edit/index.html +0 -15
  212. package/out/note/edit/index.txt +0 -24
  213. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_buildManifest.js +0 -0
  214. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_clientMiddlewareManifest.js +0 -0
  215. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → t7ZIeQpVvjz4a7-5Tt-VK}/_ssgManifest.js +0 -0
@@ -14,11 +14,12 @@ import Corestore from 'corestore'
14
14
  import Hyperdrive from 'hyperdrive'
15
15
  import b4a from 'b4a'
16
16
  import crypto from 'node:crypto'
17
- import { CID } from 'multiformats/cid'
18
17
  import fs from 'node:fs'
19
18
  import path from 'node:path'
20
19
 
21
20
  import { calculateCid, parseMostLink } from './core/cid.js'
21
+ import { normalizeChannelAttachment } from './core/channelAttachment.js'
22
+ import { getCidInfo } from './core/cidTopic.js'
22
23
  import {
23
24
  sanitizeFilename,
24
25
  validateAndSanitizePath,
@@ -34,6 +35,7 @@ import {
34
35
  IntegrityError,
35
36
  PermissionError,
36
37
  ConflictError,
38
+ StorageCapacityError,
37
39
  EngineNotInitializedError,
38
40
  } from './utils/errors.js'
39
41
  import {
@@ -66,6 +68,30 @@ import {
66
68
 
67
69
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
68
70
 
71
+ function normalizeOwnerAddress(address) {
72
+ const value = String(address || '').trim()
73
+ return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
74
+ }
75
+
76
+ function createOfflineSwarm() {
77
+ return {
78
+ connections: new Set(),
79
+ keyPair: {
80
+ publicKey: crypto.randomBytes(32),
81
+ },
82
+ on() {},
83
+ join() {
84
+ return {}
85
+ },
86
+ leave() {
87
+ return Promise.resolve()
88
+ },
89
+ destroy() {
90
+ return Promise.resolve()
91
+ },
92
+ }
93
+ }
94
+
69
95
  export class MostBoxEngine extends EventEmitter {
70
96
  #store = null
71
97
  #swarm = null
@@ -78,11 +104,13 @@ export class MostBoxEngine extends EventEmitter {
78
104
  #activeDownloads = new Map()
79
105
  #drivePromises = new Map()
80
106
  #fileDiscoveries = new Map()
107
+ #fileMonitors = new Map()
81
108
  #seedStates = new Map()
82
109
  #holdingResumeTask = null
83
110
 
84
111
  #channels = []
85
112
  #channelCores = new Map()
113
+ #channelLocalCoreKey = new Map()
86
114
  #channelDiscoveries = new Map()
87
115
  #channelChatDiscoveries = new Map()
88
116
  #channelPeers = new Map()
@@ -94,7 +122,9 @@ export class MostBoxEngine extends EventEmitter {
94
122
  * @param {object} options - 配置选项
95
123
  * @param {string} options.dataPath - 存储 P2P 数据的路径(必填)
96
124
  * @param {string} [options.downloadPath] - 默认下载路径(可选,默认为 dataPath/downloads)
97
- * @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:100GB
125
+ * @param {number} [options.maxFileSize] - 最大文件大小(字节)(默认:10GB
126
+ * @param {number} [options.capacityBytes] - 节点存储容量上限(字节)(默认:100GB)
127
+ * @param {boolean} [options.disableNetwork] - 测试用:跳过真实 Hyperswarm 网络
98
128
  */
99
129
  constructor(options) {
100
130
  super()
@@ -108,7 +138,9 @@ export class MostBoxEngine extends EventEmitter {
108
138
  downloadPath:
109
139
  options.downloadPath || path.join(options.dataPath, 'downloads'),
110
140
  maxFileSize: options.maxFileSize || MAX_FILE_SIZE,
141
+ capacityBytes: options.capacityBytes || 100 * 1024 * 1024 * 1024,
111
142
  downloadTimeout: options.downloadTimeout || DOWNLOAD_TIMEOUT,
143
+ disableNetwork: options.disableNetwork === true,
112
144
  }
113
145
  }
114
146
 
@@ -167,14 +199,19 @@ export class MostBoxEngine extends EventEmitter {
167
199
  }
168
200
 
169
201
  console.log(`[MostBox] Initializing Hyperswarm...`)
170
- this.#swarm = new Hyperswarm({
171
- maxPeers: MAX_PEERS,
172
- bootstrap: SWARM_BOOTSTRAP,
173
- firewall: () => false,
174
- connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
175
- randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
176
- handshakeTimeout: CONNECTION_TIMEOUT,
177
- })
202
+ if (this.#options.disableNetwork) {
203
+ this.#swarm = createOfflineSwarm()
204
+ this.#chatSwarm = createOfflineSwarm()
205
+ } else {
206
+ this.#swarm = new Hyperswarm({
207
+ maxPeers: MAX_PEERS,
208
+ bootstrap: SWARM_BOOTSTRAP,
209
+ firewall: () => false,
210
+ connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
211
+ randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
212
+ handshakeTimeout: CONNECTION_TIMEOUT,
213
+ })
214
+ }
178
215
 
179
216
  this.#swarm.on('error', err => {
180
217
  if (
@@ -200,14 +237,16 @@ export class MostBoxEngine extends EventEmitter {
200
237
  this.emit('connection', conn)
201
238
  })
202
239
 
203
- this.#chatSwarm = new Hyperswarm({
204
- maxPeers: MAX_PEERS,
205
- bootstrap: SWARM_BOOTSTRAP,
206
- firewall: () => false,
207
- connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
208
- randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
209
- handshakeTimeout: CONNECTION_TIMEOUT,
210
- })
240
+ if (!this.#options.disableNetwork) {
241
+ this.#chatSwarm = new Hyperswarm({
242
+ maxPeers: MAX_PEERS,
243
+ bootstrap: SWARM_BOOTSTRAP,
244
+ firewall: () => false,
245
+ connectionKeepAlive: SWARM_KEEP_ALIVE_INTERVAL,
246
+ randomPunchInterval: SWARM_RANDOM_PUNCH_INTERVAL,
247
+ handshakeTimeout: CONNECTION_TIMEOUT,
248
+ })
249
+ }
211
250
 
212
251
  this.#chatSwarm.on('error', err => {
213
252
  if (
@@ -265,9 +304,22 @@ export class MostBoxEngine extends EventEmitter {
265
304
  valueEncoding: 'json',
266
305
  })
267
306
  await core.ready()
268
- this.#channelCores.set(channel.name, core)
307
+ const coreKeyHex = b4a.toString(core.key, 'hex')
308
+ if (!this.#channelCores.has(channel.name)) {
309
+ this.#channelCores.set(channel.name, new Map())
310
+ }
311
+ this.#channelCores.get(channel.name).set(coreKeyHex, core)
312
+ this.#channelLocalCoreKey.set(channel.name, coreKeyHex)
269
313
  this.#channelPeers.set(channel.name, new Map())
270
314
  this.#setupChannelAppendListener(core, channel.name)
315
+ const remoteCoreKeys = Array.isArray(channel.remoteCoreKeys)
316
+ ? channel.remoteCoreKeys
317
+ : []
318
+ for (const remoteCoreKey of remoteCoreKeys) {
319
+ if (remoteCoreKey && remoteCoreKey !== coreKeyHex) {
320
+ await this.#openRemoteChannelCore(channel.name, remoteCoreKey)
321
+ }
322
+ }
271
323
 
272
324
  const discoveryKey = b4a.from(channel.discoveryKey, 'hex')
273
325
  const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(
@@ -315,25 +367,16 @@ export class MostBoxEngine extends EventEmitter {
315
367
  }
316
368
  this.#activeDownloads.clear()
317
369
 
370
+ await Promise.allSettled(
371
+ [...this.#fileMonitors.values()].map(item => this.#closeFileMonitor(item))
372
+ )
373
+ this.#fileMonitors.clear()
318
374
  await Promise.allSettled([...this.#drives.values()].map(d => d.close()))
319
375
  this.#drives.clear()
320
376
  this.#fileDiscoveries.clear()
321
377
  this.#seedStates.clear()
322
378
  this.#holdingResumeTask = null
323
379
 
324
- for (const core of this.#channelCores.values()) {
325
- try {
326
- await core.close()
327
- } catch (err) {
328
- console.warn('[MostBox] Failed to close channel core:', err.message)
329
- }
330
- }
331
- this.#channelCores.clear()
332
- this.#channelDiscoveries.clear()
333
- this.#channelChatDiscoveries.clear()
334
- this.#channelPeers.clear()
335
- this.#channels = []
336
-
337
380
  if (this.#swarm) {
338
381
  await this.#swarm.destroy()
339
382
  this.#swarm = null
@@ -344,6 +387,22 @@ export class MostBoxEngine extends EventEmitter {
344
387
  this.#chatSwarm = null
345
388
  }
346
389
 
390
+ for (const [, coresMap] of this.#channelCores) {
391
+ for (const [, core] of coresMap) {
392
+ try {
393
+ await core.close()
394
+ } catch (err) {
395
+ console.warn('[MostBox] Failed to close channel core:', err.message)
396
+ }
397
+ }
398
+ }
399
+ this.#channelCores.clear()
400
+ this.#channelLocalCoreKey.clear()
401
+ this.#channelDiscoveries.clear()
402
+ this.#channelChatDiscoveries.clear()
403
+ this.#channelPeers.clear()
404
+ this.#channels = []
405
+
347
406
  if (this.#store) {
348
407
  await this.#store.close()
349
408
  this.#store = null
@@ -390,6 +449,7 @@ export class MostBoxEngine extends EventEmitter {
390
449
  */
391
450
  async publishFile(content, fileName, options = {}) {
392
451
  this.#ensureInitialized()
452
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
393
453
 
394
454
  let cleanPath = null
395
455
  let safeFileName
@@ -429,6 +489,8 @@ export class MostBoxEngine extends EventEmitter {
429
489
  )
430
490
  }
431
491
 
492
+ this.#checkCapacity(fileSize)
493
+
432
494
  this.emit('publish:progress', {
433
495
  stage: 'calculating-cid',
434
496
  file: safeFileName,
@@ -442,7 +504,7 @@ export class MostBoxEngine extends EventEmitter {
442
504
 
443
505
  // 检查相同内容是否已存在
444
506
  const existingIndex = this.#publishedFiles.findIndex(
445
- f => f.cid === cidString
507
+ f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
446
508
  )
447
509
  if (existingIndex !== -1) {
448
510
  const existing = this.#publishedFiles[existingIndex]
@@ -457,7 +519,6 @@ export class MostBoxEngine extends EventEmitter {
457
519
  localPath: holdingLocalPath,
458
520
  driveName: name,
459
521
  source: 'published',
460
- temporary: false,
461
522
  })
462
523
  return {
463
524
  cid: cidString,
@@ -475,10 +536,6 @@ export class MostBoxEngine extends EventEmitter {
475
536
  server: true,
476
537
  client: false,
477
538
  })
478
- this.#swarm.join(drive.discoveryKey, {
479
- server: true,
480
- client: false,
481
- })
482
539
  }
483
540
  await this.#joinCidTopicInternal(cidString, {
484
541
  server: true,
@@ -532,6 +589,7 @@ export class MostBoxEngine extends EventEmitter {
532
589
  driveName: name,
533
590
  publishedAt: new Date().toISOString(),
534
591
  starred: false,
592
+ ownerAddress,
535
593
  })
536
594
  this.#savePublishedMetadata()
537
595
  this.#upsertHolding({
@@ -541,7 +599,6 @@ export class MostBoxEngine extends EventEmitter {
541
599
  localPath: holdingLocalPath,
542
600
  driveName: name,
543
601
  source: 'published',
544
- temporary: false,
545
602
  })
546
603
 
547
604
  const result = {
@@ -565,6 +622,7 @@ export class MostBoxEngine extends EventEmitter {
565
622
  */
566
623
  async downloadFile(link, taskId = null, options = {}) {
567
624
  this.#ensureInitialized()
625
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
568
626
 
569
627
  taskId =
570
628
  taskId || `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
@@ -585,10 +643,11 @@ export class MostBoxEngine extends EventEmitter {
585
643
  }
586
644
  const cidString = parsed.cid
587
645
  console.log(`[MostBox] Parsed CID: ${cidString}`)
588
- const parsedCid = CID.parse(cidString)
589
646
  const { driveName: name } = this.#getCidInfo(cidString)
590
647
 
591
- const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
648
+ const existingFile = this.#publishedFiles.find(
649
+ f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
650
+ )
592
651
  if (existingFile) {
593
652
  console.log(`[MostBox] File already exists: ${existingFile.fileName}`)
594
653
  const existingHolding = this.#holdings.find(
@@ -609,7 +668,6 @@ export class MostBoxEngine extends EventEmitter {
609
668
  existingHolding?.localPath || existingFile.localPath || null,
610
669
  driveName: existingFile.driveName || name,
611
670
  source: existingHolding?.source || 'published',
612
- temporary: existingHolding?.temporary === true,
613
671
  })
614
672
  return {
615
673
  taskId,
@@ -632,18 +690,11 @@ export class MostBoxEngine extends EventEmitter {
632
690
  })
633
691
 
634
692
  this.emit('download:status', { taskId, status: 'connecting' })
635
-
636
- console.log(`[MostBox] Joining swarm for drive discovery...`)
637
- this.#swarm.join(drive.discoveryKey, {
638
- server: true,
639
- client: true,
640
- })
641
- console.log(`[MostBox] Swarm join requested`)
642
693
  } else {
643
694
  console.log(`[MostBox] Using existing drive: ${name}`)
644
695
  }
645
696
  await this.#joinCidTopicInternal(cidString, {
646
- server: true,
697
+ server: false,
647
698
  client: true,
648
699
  })
649
700
 
@@ -717,6 +768,10 @@ export class MostBoxEngine extends EventEmitter {
717
768
  // 忽略
718
769
  }
719
770
 
771
+ if (totalBytes > 0) {
772
+ this.#checkCapacity(totalBytes)
773
+ }
774
+
720
775
  const savePath = path.join(targetDir, sanitizedFileName)
721
776
  fs.mkdirSync(path.dirname(savePath), { recursive: true })
722
777
  if (fs.existsSync(savePath)) {
@@ -826,13 +881,12 @@ export class MostBoxEngine extends EventEmitter {
826
881
  this.emit('download:status', { taskId, status: 'verifying' })
827
882
 
828
883
  const { cid: downloadedCid } = await calculateCid(savePath)
829
- const expectedHash = b4a.toString(parsedCid.multihash.digest, 'hex')
830
- const actualHash = b4a.toString(downloadedCid.multihash.digest, 'hex')
884
+ const downloadedCidString = downloadedCid.toString()
831
885
 
832
- if (expectedHash !== actualHash) {
886
+ if (downloadedCidString !== cidString) {
833
887
  fs.unlinkSync(savePath)
834
888
  throw new IntegrityError(
835
- `File content CID mismatch. File may be corrupted or tampered.`
889
+ `File content CID mismatch. Expected ${cidString}, got ${downloadedCidString}.`
836
890
  )
837
891
  }
838
892
 
@@ -848,7 +902,17 @@ export class MostBoxEngine extends EventEmitter {
848
902
  writeStream.on('error', reject)
849
903
  readStream.on('error', reject)
850
904
  })
905
+ const verifyEntry = await drive.entry(driveKey)
906
+ if (!verifyEntry || !verifyEntry.value || !verifyEntry.value.blob) {
907
+ throw new IntegrityError(
908
+ `Failed to write file to Hyperdrive for seeding: ${driveKey}`
909
+ )
910
+ }
851
911
  }
912
+ await this.#joinCidTopicInternal(cidString, {
913
+ server: true,
914
+ client: false,
915
+ })
852
916
 
853
917
  const result = {
854
918
  taskId,
@@ -858,7 +922,7 @@ export class MostBoxEngine extends EventEmitter {
858
922
 
859
923
  // 将下载的文件添加到已发布文件列表(displayName 用原始文件名)
860
924
  const existingIndex = this.#publishedFiles.findIndex(
861
- f => f.cid === cidString
925
+ f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
862
926
  )
863
927
  if (existingIndex !== -1) {
864
928
  const existing = this.#publishedFiles[existingIndex]
@@ -873,6 +937,7 @@ export class MostBoxEngine extends EventEmitter {
873
937
  driveName: name,
874
938
  publishedAt: new Date().toISOString(),
875
939
  starred: false,
940
+ ownerAddress,
876
941
  })
877
942
  }
878
943
  this.#savePublishedMetadata()
@@ -884,7 +949,6 @@ export class MostBoxEngine extends EventEmitter {
884
949
  localPath: savePath,
885
950
  driveName: name,
886
951
  source: 'downloaded',
887
- temporary: true,
888
952
  })
889
953
 
890
954
  this.emit('download:success', result)
@@ -904,6 +968,7 @@ export class MostBoxEngine extends EventEmitter {
904
968
  */
905
969
  async checkDownloadAvailability(link, options = {}) {
906
970
  this.#ensureInitialized()
971
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
907
972
 
908
973
  const timeout = options.timeout || DRIVE_ENTRY_TIMEOUT
909
974
  const parsed = parseMostLink(link)
@@ -913,7 +978,9 @@ export class MostBoxEngine extends EventEmitter {
913
978
 
914
979
  const cidString = parsed.cid
915
980
  const { driveName: name } = this.#getCidInfo(cidString)
916
- const existingFile = this.#publishedFiles.find(f => f.cid === cidString)
981
+ const existingFile = this.#publishedFiles.find(
982
+ f => f.cid === cidString && this.#recordMatchesOwner(f, ownerAddress)
983
+ )
917
984
  if (existingFile) {
918
985
  return {
919
986
  available: true,
@@ -938,15 +1005,10 @@ export class MostBoxEngine extends EventEmitter {
938
1005
  server: true,
939
1006
  client: true,
940
1007
  })
941
-
942
- this.#swarm.join(drive.discoveryKey, {
943
- server: true,
944
- client: true,
945
- })
946
1008
  }
947
1009
 
948
1010
  await this.#joinCidTopicInternal(cidString, {
949
- server: true,
1011
+ server: false,
950
1012
  client: true,
951
1013
  })
952
1014
 
@@ -984,6 +1046,11 @@ export class MostBoxEngine extends EventEmitter {
984
1046
  listPublishedFiles(options = {}) {
985
1047
  this.#ensureInitialized()
986
1048
  let files = this.#publishedFiles
1049
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1050
+
1051
+ if (ownerAddress) {
1052
+ files = files.filter(f => this.#recordMatchesOwner(f, ownerAddress))
1053
+ }
987
1054
 
988
1055
  if (options.starred === true) {
989
1056
  files = files.filter(f => f.starred === true)
@@ -995,6 +1062,7 @@ export class MostBoxEngine extends EventEmitter {
995
1062
  link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
996
1063
  publishedAt: f.publishedAt,
997
1064
  starred: f.starred || false,
1065
+ ownerAddress: f.ownerAddress || '',
998
1066
  }))
999
1067
  }
1000
1068
 
@@ -1003,9 +1071,12 @@ export class MostBoxEngine extends EventEmitter {
1003
1071
  * @param {string} cid - 文件的 CID
1004
1072
  * @returns {object} 更新后的文件信息
1005
1073
  */
1006
- toggleStarred(cid) {
1074
+ toggleStarred(cid, options = {}) {
1007
1075
  this.#ensureInitialized()
1008
- const index = this.#publishedFiles.findIndex(f => f.cid === cid)
1076
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1077
+ const index = this.#publishedFiles.findIndex(
1078
+ f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1079
+ )
1009
1080
  if (index === -1) {
1010
1081
  throw new Error('File not found')
1011
1082
  }
@@ -1022,9 +1093,12 @@ export class MostBoxEngine extends EventEmitter {
1022
1093
  * @param {string} cid - 要删除文件的 CID
1023
1094
  * @returns {Promise<Array>} 更新后的已发布文件列表
1024
1095
  */
1025
- async deletePublishedFile(cid) {
1096
+ async deletePublishedFile(cid, options = {}) {
1026
1097
  this.#ensureInitialized()
1027
- const index = this.#publishedFiles.findIndex(f => f.cid === cid)
1098
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1099
+ const index = this.#publishedFiles.findIndex(
1100
+ f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1101
+ )
1028
1102
  if (index !== -1) {
1029
1103
  const fileRecord = this.#publishedFiles[index]
1030
1104
  const holding = this.#holdings.find(item => item.cid === fileRecord.cid)
@@ -1039,34 +1113,42 @@ export class MostBoxEngine extends EventEmitter {
1039
1113
  source: holding?.source || 'published',
1040
1114
  publishedAt: fileRecord.publishedAt,
1041
1115
  starred: fileRecord.starred || false,
1116
+ ownerAddress: fileRecord.ownerAddress || ownerAddress,
1042
1117
  deletedAt: new Date().toISOString(),
1043
1118
  })
1044
1119
  this.#saveTrashMetadata()
1045
1120
 
1046
- await this.#leaveCidTopic(fileRecord.cid)
1047
- await this.#closeDriveForSeed(
1048
- fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1049
- )
1050
- this.#removeHolding(fileRecord.cid)
1051
-
1052
1121
  this.#publishedFiles.splice(index, 1)
1053
1122
  this.#savePublishedMetadata()
1123
+
1124
+ if (!this.#hasPublishedReference(fileRecord.cid)) {
1125
+ await this.#leaveCidTopic(fileRecord.cid)
1126
+ await this.#closeDriveForSeed(
1127
+ fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1128
+ )
1129
+ this.#removeHolding(fileRecord.cid)
1130
+ }
1054
1131
  }
1055
- return this.listPublishedFiles()
1132
+ return this.listPublishedFiles({ ownerAddress })
1056
1133
  }
1057
1134
 
1058
1135
  /**
1059
1136
  * 列出回收站中的所有文件
1060
1137
  * @returns {Array} 回收站文件
1061
1138
  */
1062
- listTrashFiles() {
1139
+ listTrashFiles(options = {}) {
1063
1140
  this.#ensureInitialized()
1064
- return this.#trashFiles.map(f => ({
1141
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1142
+ const files = ownerAddress
1143
+ ? this.#trashFiles.filter(f => this.#recordMatchesOwner(f, ownerAddress))
1144
+ : this.#trashFiles
1145
+ return files.map(f => ({
1065
1146
  fileName: f.fileName,
1066
1147
  cid: f.cid,
1067
1148
  link: `most://${f.cid}?filename=${encodeURIComponent(f.fileName)}`,
1068
1149
  publishedAt: f.publishedAt,
1069
1150
  starred: f.starred || false,
1151
+ ownerAddress: f.ownerAddress || '',
1070
1152
  deletedAt: f.deletedAt,
1071
1153
  }))
1072
1154
  }
@@ -1076,18 +1158,19 @@ export class MostBoxEngine extends EventEmitter {
1076
1158
  * @param {string} cid - 要恢复文件的 CID
1077
1159
  * @returns {Promise<Array>} 更新后的已发布文件列表
1078
1160
  */
1079
- async restoreTrashFile(cid) {
1161
+ async restoreTrashFile(cid, options = {}) {
1080
1162
  this.#ensureInitialized()
1081
- const index = this.#trashFiles.findIndex(f => f.cid === cid)
1163
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1164
+ const index = this.#trashFiles.findIndex(
1165
+ f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1166
+ )
1082
1167
  if (index === -1) {
1083
1168
  throw new Error('File not found in trash')
1084
1169
  }
1085
1170
 
1086
1171
  const fileRecord = this.#trashFiles[index]
1087
1172
 
1088
- const parsedCid = CID.parse(fileRecord.cid)
1089
- const hashHex = b4a.toString(parsedCid.multihash.digest, 'hex')
1090
- const driveName = `drive-${hashHex}`
1173
+ const { driveName } = this.#getCidInfo(fileRecord.cid)
1091
1174
 
1092
1175
  this.#publishedFiles.push({
1093
1176
  fileName: fileRecord.fileName,
@@ -1095,6 +1178,7 @@ export class MostBoxEngine extends EventEmitter {
1095
1178
  driveName,
1096
1179
  publishedAt: fileRecord.publishedAt,
1097
1180
  starred: fileRecord.starred || false,
1181
+ ownerAddress: fileRecord.ownerAddress || ownerAddress,
1098
1182
  })
1099
1183
  this.#savePublishedMetadata()
1100
1184
 
@@ -1112,10 +1196,9 @@ export class MostBoxEngine extends EventEmitter {
1112
1196
  localPath: fileRecord.localPath || null,
1113
1197
  driveName,
1114
1198
  source: fileRecord.source || 'published',
1115
- temporary: fileRecord.source === 'downloaded',
1116
1199
  })
1117
1200
 
1118
- return this.listPublishedFiles()
1201
+ return this.listPublishedFiles({ ownerAddress })
1119
1202
  }
1120
1203
 
1121
1204
  /**
@@ -1123,41 +1206,60 @@ export class MostBoxEngine extends EventEmitter {
1123
1206
  * @param {string} cid - 要永久删除文件的 CID
1124
1207
  * @returns {Promise<Array>} 更新后的回收站列表
1125
1208
  */
1126
- async permanentDeleteTrashFile(cid) {
1209
+ async permanentDeleteTrashFile(cid, options = {}) {
1127
1210
  this.#ensureInitialized()
1128
- const index = this.#trashFiles.findIndex(f => f.cid === cid)
1211
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1212
+ const index = this.#trashFiles.findIndex(
1213
+ f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1214
+ )
1129
1215
  if (index !== -1) {
1130
1216
  const fileRecord = this.#trashFiles[index]
1131
1217
  const driveName =
1132
1218
  fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1133
1219
 
1134
- try {
1135
- const drive = await this.#getOrCreateDrive(driveName)
1136
- await drive.del('/' + fileRecord.cid)
1137
- } catch {
1138
- // 文件可能不存在于驱动器中
1139
- }
1140
- await this.#closeDriveForSeed(driveName)
1141
-
1142
- await this.#leaveCidTopic(fileRecord.cid)
1143
- this.#removeHolding(fileRecord.cid)
1144
1220
  this.#trashFiles.splice(index, 1)
1145
1221
  this.#saveTrashMetadata()
1222
+
1223
+ if (!this.#hasAnyUserReference(fileRecord.cid)) {
1224
+ try {
1225
+ const drive = await this.#getOrCreateDrive(driveName)
1226
+ await drive.del('/' + fileRecord.cid)
1227
+ } catch {
1228
+ // 文件可能不存在于驱动器中
1229
+ }
1230
+ await this.#closeDriveForSeed(driveName)
1231
+ await this.#leaveCidTopic(fileRecord.cid)
1232
+ this.#removeHolding(fileRecord.cid)
1233
+ }
1146
1234
  }
1147
- return this.listTrashFiles()
1235
+ return this.listTrashFiles({ ownerAddress })
1148
1236
  }
1149
1237
 
1150
1238
  /**
1151
1239
  * 清空回收站 — 永久删除所有回收站文件
1152
1240
  * @returns {Promise<Array>} 清空后的回收站列表
1153
1241
  */
1154
- async emptyTrash() {
1242
+ async emptyTrash(options = {}) {
1155
1243
  this.#ensureInitialized()
1244
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1245
+ const remainingTrash = []
1246
+ const removedTrash = []
1156
1247
 
1157
1248
  for (const fileRecord of this.#trashFiles) {
1249
+ if (ownerAddress && !this.#recordMatchesOwner(fileRecord, ownerAddress)) {
1250
+ remainingTrash.push(fileRecord)
1251
+ continue
1252
+ }
1253
+ removedTrash.push(fileRecord)
1254
+ }
1255
+
1256
+ this.#trashFiles = remainingTrash
1257
+ this.#saveTrashMetadata()
1258
+
1259
+ for (const fileRecord of removedTrash) {
1260
+ if (this.#hasAnyUserReference(fileRecord.cid)) continue
1158
1261
  const driveName =
1159
1262
  fileRecord.driveName || this.#getCidInfo(fileRecord.cid).driveName
1160
-
1161
1263
  try {
1162
1264
  const drive = await this.#getOrCreateDrive(driveName)
1163
1265
  await drive.del('/' + fileRecord.cid)
@@ -1169,18 +1271,16 @@ export class MostBoxEngine extends EventEmitter {
1169
1271
  this.#removeHolding(fileRecord.cid)
1170
1272
  }
1171
1273
 
1172
- this.#trashFiles = []
1173
- this.#saveTrashMetadata()
1174
-
1175
- return []
1274
+ return this.listTrashFiles({ ownerAddress })
1176
1275
  }
1177
1276
 
1178
1277
  /**
1179
1278
  * 获取存储统计信息
1180
1279
  * @returns {Promise<{ total: number, used: number, free: number, fileCount: number, trashCount: number }>}
1181
1280
  */
1182
- async getStorageStats() {
1281
+ async getStorageStats(options = {}) {
1183
1282
  this.#ensureInitialized()
1283
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1184
1284
 
1185
1285
  let totalSize = 0
1186
1286
  let freeSize = 0
@@ -1231,8 +1331,16 @@ export class MostBoxEngine extends EventEmitter {
1231
1331
  total: totalSize,
1232
1332
  used: usedSize,
1233
1333
  free: freeSize,
1234
- fileCount: this.#publishedFiles.length,
1235
- trashCount: this.#trashFiles.length,
1334
+ fileCount: ownerAddress
1335
+ ? this.#publishedFiles.filter(f =>
1336
+ this.#recordMatchesOwner(f, ownerAddress)
1337
+ ).length
1338
+ : this.#publishedFiles.length,
1339
+ trashCount: ownerAddress
1340
+ ? this.#trashFiles.filter(f =>
1341
+ this.#recordMatchesOwner(f, ownerAddress)
1342
+ ).length
1343
+ : this.#trashFiles.length,
1236
1344
  }
1237
1345
  }
1238
1346
 
@@ -1243,9 +1351,12 @@ export class MostBoxEngine extends EventEmitter {
1243
1351
  * @param {string} newFileName - 新文件路径
1244
1352
  * @returns {object} 更新后的文件信息
1245
1353
  */
1246
- moveFile(cid, newFileName) {
1354
+ moveFile(cid, newFileName, options = {}) {
1247
1355
  this.#ensureInitialized()
1248
- const index = this.#publishedFiles.findIndex(f => f.cid === cid)
1356
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1357
+ const index = this.#publishedFiles.findIndex(
1358
+ f => f.cid === cid && this.#recordMatchesOwner(f, ownerAddress)
1359
+ )
1249
1360
  if (index === -1) {
1250
1361
  throw new Error('File not found')
1251
1362
  }
@@ -1267,13 +1378,17 @@ export class MostBoxEngine extends EventEmitter {
1267
1378
  * @param {string} newPath - 新文件夹路径
1268
1379
  * @returns {object} 更新后的文件信息
1269
1380
  */
1270
- renameFolder(oldPath, newPath) {
1381
+ renameFolder(oldPath, newPath, options = {}) {
1271
1382
  this.#ensureInitialized()
1383
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1272
1384
  const prefix = oldPath + '/'
1273
1385
  const updatedFiles = []
1274
1386
 
1275
1387
  for (const file of this.#publishedFiles) {
1276
- if (file.fileName.startsWith(prefix)) {
1388
+ if (
1389
+ file.fileName.startsWith(prefix) &&
1390
+ this.#recordMatchesOwner(file, ownerAddress)
1391
+ ) {
1277
1392
  const remainder = file.fileName.substring(prefix.length)
1278
1393
  const newFileName = sanitizeFilename(
1279
1394
  remainder ? newPath + '/' + remainder : newPath
@@ -1324,8 +1439,114 @@ export class MostBoxEngine extends EventEmitter {
1324
1439
  this.#options.maxFileSize = Math.floor(parsed)
1325
1440
  }
1326
1441
 
1327
- getPublishedFiles() {
1328
- return this.#publishedFiles
1442
+ getPublishedFiles(options = {}) {
1443
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1444
+ return ownerAddress
1445
+ ? this.#publishedFiles.filter(f =>
1446
+ this.#recordMatchesOwner(f, ownerAddress)
1447
+ )
1448
+ : this.#publishedFiles
1449
+ }
1450
+
1451
+ listUsers() {
1452
+ this.#ensureInitialized()
1453
+ const users = new Map()
1454
+ const ensure = address => {
1455
+ const ownerAddress = normalizeOwnerAddress(address)
1456
+ if (!ownerAddress) return null
1457
+ if (!users.has(ownerAddress)) {
1458
+ users.set(ownerAddress, {
1459
+ address: ownerAddress,
1460
+ fileCount: 0,
1461
+ trashCount: 0,
1462
+ cidCount: 0,
1463
+ cids: new Set(),
1464
+ })
1465
+ }
1466
+ return users.get(ownerAddress)
1467
+ }
1468
+
1469
+ for (const file of this.#publishedFiles) {
1470
+ const entry = ensure(file.ownerAddress)
1471
+ if (!entry) continue
1472
+ entry.fileCount += 1
1473
+ entry.cids.add(file.cid)
1474
+ }
1475
+ for (const file of this.#trashFiles) {
1476
+ const entry = ensure(file.ownerAddress)
1477
+ if (!entry) continue
1478
+ entry.trashCount += 1
1479
+ entry.cids.add(file.cid)
1480
+ }
1481
+
1482
+ return [...users.values()].map(user => ({
1483
+ address: user.address,
1484
+ fileCount: user.fileCount,
1485
+ trashCount: user.trashCount,
1486
+ cidCount: user.cids.size,
1487
+ }))
1488
+ }
1489
+
1490
+ async clearUserData(ownerAddressInput) {
1491
+ this.#ensureInitialized()
1492
+ const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
1493
+ if (!ownerAddress) {
1494
+ throw new ValidationError('valid owner address is required')
1495
+ }
1496
+
1497
+ const affectedCids = new Set()
1498
+ const beforeFiles = this.#publishedFiles.length
1499
+ const beforeTrash = this.#trashFiles.length
1500
+
1501
+ this.#publishedFiles = this.#publishedFiles.filter(file => {
1502
+ if (this.#recordMatchesOwner(file, ownerAddress)) {
1503
+ affectedCids.add(file.cid)
1504
+ return false
1505
+ }
1506
+ return true
1507
+ })
1508
+ this.#trashFiles = this.#trashFiles.filter(file => {
1509
+ if (this.#recordMatchesOwner(file, ownerAddress)) {
1510
+ affectedCids.add(file.cid)
1511
+ return false
1512
+ }
1513
+ return true
1514
+ })
1515
+ this.#channels = this.#channels
1516
+ .map(channel => ({
1517
+ ...channel,
1518
+ members: Array.isArray(channel.members)
1519
+ ? channel.members.filter(
1520
+ member => normalizeOwnerAddress(member) !== ownerAddress
1521
+ )
1522
+ : [],
1523
+ }))
1524
+ .filter(channel => channel.members.length > 0)
1525
+
1526
+ this.#savePublishedMetadata()
1527
+ this.#saveTrashMetadata()
1528
+ this.#saveChannelsMetadata()
1529
+
1530
+ let removedReplicas = 0
1531
+ for (const cid of affectedCids) {
1532
+ if (this.#hasAnyUserReference(cid)) continue
1533
+ const driveName = this.#getCidInfo(cid).driveName
1534
+ try {
1535
+ const drive = await this.#getOrCreateDrive(driveName)
1536
+ await drive.del('/' + cid)
1537
+ } catch {}
1538
+ await this.#closeDriveForSeed(driveName)
1539
+ await this.#leaveCidTopic(cid)
1540
+ this.#removeHolding(cid)
1541
+ removedReplicas += 1
1542
+ }
1543
+
1544
+ return {
1545
+ ownerAddress,
1546
+ removedFiles: beforeFiles - this.#publishedFiles.length,
1547
+ removedTrashFiles: beforeTrash - this.#trashFiles.length,
1548
+ removedReplicas,
1549
+ }
1329
1550
  }
1330
1551
 
1331
1552
  /**
@@ -1345,6 +1566,7 @@ export class MostBoxEngine extends EventEmitter {
1345
1566
  seedStatus: status,
1346
1567
  seedError: seedState?.error,
1347
1568
  seedStatusUpdatedAt: seedState?.updatedAt,
1569
+ ...this.#getFileRuntimeStats(holding.cid),
1348
1570
  link: `most://${holding.cid}?filename=${encodeURIComponent(holding.fileName || holding.cid)}`,
1349
1571
  }
1350
1572
  })
@@ -1383,6 +1605,7 @@ export class MostBoxEngine extends EventEmitter {
1383
1605
  }
1384
1606
  const result = await this.downloadFile(input.link, input.taskId || null, {
1385
1607
  timeout: input.timeout,
1608
+ ownerAddress: input.ownerAddress,
1386
1609
  })
1387
1610
  return {
1388
1611
  ...result,
@@ -1400,6 +1623,7 @@ export class MostBoxEngine extends EventEmitter {
1400
1623
  const link = `most://${cid}?filename=${encodeURIComponent(fileName)}`
1401
1624
  const result = await this.downloadFile(link, input.taskId || null, {
1402
1625
  timeout: input.timeout,
1626
+ ownerAddress: input.ownerAddress,
1403
1627
  })
1404
1628
 
1405
1629
  return {
@@ -1447,10 +1671,20 @@ export class MostBoxEngine extends EventEmitter {
1447
1671
  * @param {number} [offset=0] - 读取起始位置
1448
1672
  * @param {number} [limit=10000] - 最大读取字节数
1449
1673
  */
1450
- async readFileContent(cid, offset = 0, limit = DEFAULT_READ_LIMIT) {
1674
+ async readFileContent(
1675
+ cid,
1676
+ offset = 0,
1677
+ limit = DEFAULT_READ_LIMIT,
1678
+ options = {}
1679
+ ) {
1451
1680
  this.#ensureInitialized()
1681
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1452
1682
 
1453
- const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
1683
+ const fileRecord = this.#publishedFiles.find(
1684
+ f =>
1685
+ f.cid === cid &&
1686
+ (options.public || this.#recordMatchesOwner(f, ownerAddress))
1687
+ )
1454
1688
  if (!fileRecord) {
1455
1689
  throw new Error('File not found')
1456
1690
  }
@@ -1507,8 +1741,13 @@ export class MostBoxEngine extends EventEmitter {
1507
1741
  */
1508
1742
  async readFileRaw(cid, options = {}) {
1509
1743
  this.#ensureInitialized()
1744
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1510
1745
 
1511
- const fileRecord = this.#publishedFiles.find(f => f.cid === cid)
1746
+ const fileRecord = this.#publishedFiles.find(
1747
+ f =>
1748
+ f.cid === cid &&
1749
+ (options.public || this.#recordMatchesOwner(f, ownerAddress))
1750
+ )
1512
1751
  if (!fileRecord) {
1513
1752
  throw new Error('File not found')
1514
1753
  }
@@ -1592,8 +1831,9 @@ export class MostBoxEngine extends EventEmitter {
1592
1831
  * @param {string} [type='personal'] - 频道类型
1593
1832
  * @returns {Promise<{ name: string, key: string }>}
1594
1833
  */
1595
- async createChannel(name, type = 'personal') {
1834
+ async createChannel(name, type = 'personal', options = {}) {
1596
1835
  this.#ensureInitialized()
1836
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1597
1837
 
1598
1838
  if (!CHANNEL_NAME_REGEX.test(name)) {
1599
1839
  throw new Error('频道名只能包含字母、数字、下划线和连字符')
@@ -1607,6 +1847,13 @@ export class MostBoxEngine extends EventEmitter {
1607
1847
 
1608
1848
  const existing = this.#channels.find(c => c.name === name)
1609
1849
  if (existing) {
1850
+ if (ownerAddress && !Array.isArray(existing.members)) {
1851
+ existing.members = []
1852
+ }
1853
+ if (ownerAddress && !existing.members.includes(ownerAddress)) {
1854
+ existing.members.push(ownerAddress)
1855
+ this.#saveChannelsMetadata()
1856
+ }
1610
1857
  return { name: existing.name, key: existing.coreKey }
1611
1858
  }
1612
1859
 
@@ -1633,10 +1880,18 @@ export class MostBoxEngine extends EventEmitter {
1633
1880
  coreKey: b4a.toString(core.key, 'hex'),
1634
1881
  createdAt: new Date().toISOString(),
1635
1882
  type,
1883
+ ownerAddress,
1884
+ members: ownerAddress ? [ownerAddress] : [],
1885
+ remoteCoreKeys: [],
1636
1886
  }
1637
1887
 
1638
1888
  this.#channels.push(channelInfo)
1639
- this.#channelCores.set(name, core)
1889
+ const coreKeyHex = b4a.toString(core.key, 'hex')
1890
+ if (!this.#channelCores.has(name)) {
1891
+ this.#channelCores.set(name, new Map())
1892
+ }
1893
+ this.#channelCores.get(name).set(coreKeyHex, core)
1894
+ this.#channelLocalCoreKey.set(name, coreKeyHex)
1640
1895
  this.#channelPeers.set(name, new Map())
1641
1896
  this.#channelDiscoveries.set(name, appDiscovery)
1642
1897
  this.#channelChatDiscoveries.set(name, chatDiscovery)
@@ -1654,11 +1909,29 @@ export class MostBoxEngine extends EventEmitter {
1654
1909
  * @param {string} [coreKey] - 频道的 coreKey(加入他人创建的频道时必填)
1655
1910
  * @returns {Promise<{ name: string, key: string }>}
1656
1911
  */
1657
- async joinChannel(name, coreKey = null) {
1912
+ async joinChannel(name, coreKey = null, options = {}) {
1658
1913
  this.#ensureInitialized()
1914
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1659
1915
 
1660
1916
  const existing = this.#channels.find(c => c.name === name)
1661
1917
  if (existing) {
1918
+ if (ownerAddress && !Array.isArray(existing.members)) {
1919
+ existing.members = []
1920
+ }
1921
+ if (ownerAddress && !existing.members.includes(ownerAddress)) {
1922
+ existing.members.push(ownerAddress)
1923
+ this.#saveChannelsMetadata()
1924
+ }
1925
+ if (coreKey && coreKey !== existing.coreKey) {
1926
+ if (!Array.isArray(existing.remoteCoreKeys)) {
1927
+ existing.remoteCoreKeys = []
1928
+ }
1929
+ if (!existing.remoteCoreKeys.includes(coreKey)) {
1930
+ existing.remoteCoreKeys.push(coreKey)
1931
+ this.#saveChannelsMetadata()
1932
+ }
1933
+ await this.#openRemoteChannelCore(name, coreKey)
1934
+ }
1662
1935
  return { name: existing.name, key: existing.coreKey }
1663
1936
  }
1664
1937
 
@@ -1667,11 +1940,13 @@ export class MostBoxEngine extends EventEmitter {
1667
1940
  }
1668
1941
 
1669
1942
  const ns = this.#store.namespace(`channel-${name}`)
1670
- const core = ns.get({
1671
- key: b4a.from(coreKey, 'hex'),
1943
+ const remoteCoreKeyHex = b4a.toString(b4a.from(coreKey, 'hex'), 'hex')
1944
+ const localCore = ns.get({
1945
+ name: `messages-${this.getNodeId()}`,
1672
1946
  valueEncoding: 'json',
1673
1947
  })
1674
- await core.ready()
1948
+ await localCore.ready()
1949
+ const localCoreKeyHex = b4a.toString(localCore.key, 'hex')
1675
1950
 
1676
1951
  const discoveryKey = this.#generateChannelDiscoveryKey(name)
1677
1952
  const chatDiscoveryKey = this.#generateChannelChatDiscoveryKey(name)
@@ -1684,27 +1959,38 @@ export class MostBoxEngine extends EventEmitter {
1684
1959
  client: true,
1685
1960
  })
1686
1961
 
1687
- this.#setupChannelAppendListener(core, name)
1962
+ this.#setupChannelAppendListener(localCore, name)
1688
1963
 
1689
1964
  const channelInfo = {
1690
1965
  name,
1691
1966
  discoveryKey: b4a.toString(discoveryKey, 'hex'),
1692
- coreKey,
1967
+ coreKey: localCoreKeyHex,
1693
1968
  createdAt: new Date().toISOString(),
1694
1969
  type: 'group',
1970
+ ownerAddress,
1971
+ members: ownerAddress ? [ownerAddress] : [],
1972
+ remoteCoreKeys:
1973
+ remoteCoreKeyHex === localCoreKeyHex ? [] : [remoteCoreKeyHex],
1695
1974
  }
1696
1975
 
1697
1976
  this.#channels.push(channelInfo)
1698
- this.#channelCores.set(name, core)
1977
+ if (!this.#channelCores.has(name)) {
1978
+ this.#channelCores.set(name, new Map())
1979
+ }
1980
+ this.#channelCores.get(name).set(localCoreKeyHex, localCore)
1981
+ this.#channelLocalCoreKey.set(name, localCoreKeyHex)
1699
1982
  this.#channelPeers.set(name, new Map())
1700
1983
  this.#channelDiscoveries.set(name, appDiscovery)
1701
1984
  this.#channelChatDiscoveries.set(name, chatDiscovery)
1702
1985
  this.#saveChannelsMetadata()
1986
+ if (remoteCoreKeyHex !== localCoreKeyHex) {
1987
+ await this.#openRemoteChannelCore(name, remoteCoreKeyHex)
1988
+ }
1703
1989
 
1704
1990
  console.log(`[MostBox] Joined channel: ${name}`)
1705
- this.emit('channel:joined', { name, key: coreKey })
1991
+ this.emit('channel:joined', { name, key: localCoreKeyHex })
1706
1992
 
1707
- return { name, key: coreKey }
1993
+ return { name, key: localCoreKeyHex }
1708
1994
  }
1709
1995
 
1710
1996
  /**
@@ -1712,8 +1998,9 @@ export class MostBoxEngine extends EventEmitter {
1712
1998
  * @param {string} name - 频道名
1713
1999
  * @returns {Promise<string[]>} 剩余频道列表
1714
2000
  */
1715
- async leaveChannel(name) {
2001
+ async leaveChannel(name, options = {}) {
1716
2002
  this.#ensureInitialized()
2003
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1717
2004
 
1718
2005
  const index = this.#channels.findIndex(c => c.name === name)
1719
2006
  if (index === -1) {
@@ -1721,6 +2008,15 @@ export class MostBoxEngine extends EventEmitter {
1721
2008
  }
1722
2009
 
1723
2010
  const channel = this.#channels[index]
2011
+ if (ownerAddress && Array.isArray(channel.members)) {
2012
+ channel.members = channel.members.filter(
2013
+ member => normalizeOwnerAddress(member) !== ownerAddress
2014
+ )
2015
+ if (channel.members.length > 0) {
2016
+ this.#saveChannelsMetadata()
2017
+ return this.listChannels({ ownerAddress })
2018
+ }
2019
+ }
1724
2020
 
1725
2021
  const appDiscovery = this.#channelDiscoveries.get(name)
1726
2022
  if (appDiscovery && this.#swarm) {
@@ -1745,18 +2041,21 @@ export class MostBoxEngine extends EventEmitter {
1745
2041
  })
1746
2042
  }
1747
2043
 
1748
- const core = this.#channelCores.get(name)
1749
- if (core) {
1750
- try {
1751
- await core.close()
1752
- } catch (err) {
1753
- console.warn(
1754
- `[MostBox] Failed to close channel core for ${name}:`,
1755
- err.message
1756
- )
2044
+ const coresMap = this.#channelCores.get(name)
2045
+ if (coresMap) {
2046
+ for (const [, core] of coresMap) {
2047
+ try {
2048
+ await core.close()
2049
+ } catch (err) {
2050
+ console.warn(
2051
+ `[MostBox] Failed to close channel core for ${name}:`,
2052
+ err.message
2053
+ )
2054
+ }
1757
2055
  }
1758
2056
  this.#channelCores.delete(name)
1759
2057
  }
2058
+ this.#channelLocalCoreKey.delete(name)
1760
2059
 
1761
2060
  this.#channelPeers.delete(name)
1762
2061
  this.#channels.splice(index, 1)
@@ -1765,23 +2064,61 @@ export class MostBoxEngine extends EventEmitter {
1765
2064
  console.log(`[MostBox] Left channel: ${name}`)
1766
2065
  this.emit('channel:left', { name })
1767
2066
 
1768
- return this.listChannels()
2067
+ return this.listChannels({ ownerAddress })
2068
+ }
2069
+
2070
+ setChannelRemark(name, remark, options = {}) {
2071
+ this.#ensureInitialized()
2072
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
2073
+ if (!ownerAddress) {
2074
+ throw new Error('需要登录才能设置备注')
2075
+ }
2076
+
2077
+ const channel = this.#channels.find(c => c.name === name)
2078
+ if (!channel) {
2079
+ throw new Error('频道不存在')
2080
+ }
2081
+
2082
+ const trimmed = (remark || '').trim()
2083
+ if (trimmed.length > 50) {
2084
+ throw new Error('备注最多 50 个字符')
2085
+ }
2086
+
2087
+ if (!channel.remarks) {
2088
+ channel.remarks = {}
2089
+ }
2090
+
2091
+ if (trimmed) {
2092
+ channel.remarks[ownerAddress] = trimmed
2093
+ } else {
2094
+ delete channel.remarks[ownerAddress]
2095
+ }
2096
+
2097
+ this.#saveChannelsMetadata()
2098
+ return trimmed
1769
2099
  }
1770
2100
 
1771
2101
  /**
1772
2102
  * 列出所有频道
1773
- * @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number }>}
2103
+ * @returns {Array<{ name: string, coreKey: string, createdAt: string, type: string, peerCount: number, remark: string }>}
1774
2104
  */
1775
- listChannels() {
2105
+ listChannels(options = {}) {
1776
2106
  this.#ensureInitialized()
2107
+ const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1777
2108
 
1778
- return this.#channels.map(c => ({
1779
- name: c.name,
1780
- coreKey: c.coreKey,
1781
- createdAt: c.createdAt,
1782
- type: c.type,
1783
- peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
1784
- }))
2109
+ return this.#channels
2110
+ .filter(c => {
2111
+ if (!ownerAddress) return true
2112
+ return Array.isArray(c.members) && c.members.includes(ownerAddress)
2113
+ })
2114
+ .map(c => ({
2115
+ name: c.name,
2116
+ coreKey: c.coreKey,
2117
+ createdAt: c.createdAt,
2118
+ type: c.type,
2119
+ peerCount: (this.#channelPeers.get(c.name) || new Map()).size,
2120
+ remark: ownerAddress && c.remarks ? c.remarks[ownerAddress] || '' : '',
2121
+ }))
1785
2122
  }
1786
2123
 
1787
2124
  /**
@@ -1794,29 +2131,48 @@ export class MostBoxEngine extends EventEmitter {
1794
2131
  */
1795
2132
  async getChannelMessages(name, options = {}) {
1796
2133
  this.#ensureInitialized()
2134
+ this.#assertChannelMember(name, options.ownerAddress)
1797
2135
 
1798
2136
  const { limit = CHANNEL_MESSAGE_LIMIT, offset = 0 } = options
1799
2137
 
1800
- const core = this.#channelCores.get(name)
1801
- if (!core) {
2138
+ const coresMap = this.#channelCores.get(name)
2139
+ if (!coresMap || coresMap.size === 0) {
1802
2140
  throw new Error('频道未初始化')
1803
2141
  }
1804
2142
 
1805
- const messages = []
1806
- const total = core.length
1807
- const start = Math.max(0, total - offset - limit)
1808
- const end = total - offset
1809
-
1810
- for (let i = start; i < end; i++) {
1811
- try {
1812
- const entry = await core.get(i)
1813
- messages.push(entry)
1814
- } catch {
1815
- break
2143
+ const allMessages = []
2144
+ for (const [coreKeyHex, core] of coresMap) {
2145
+ for (let i = 0; i < core.length; i++) {
2146
+ try {
2147
+ const entry = await core.get(i)
2148
+ if (entry && entry.type === 'message') {
2149
+ allMessages.push({
2150
+ ...entry,
2151
+ _coreKey: coreKeyHex,
2152
+ _index: i,
2153
+ })
2154
+ }
2155
+ } catch {
2156
+ break
2157
+ }
1816
2158
  }
1817
2159
  }
1818
2160
 
1819
- return messages
2161
+ const seen = new Set()
2162
+ const unique = allMessages.filter(m => {
2163
+ const key = `${m._coreKey}:${m.author}:${m.timestamp}:${m.content}`
2164
+ if (seen.has(key)) return false
2165
+ seen.add(key)
2166
+ return true
2167
+ })
2168
+
2169
+ unique.sort((a, b) => a.timestamp - b.timestamp)
2170
+
2171
+ const total = unique.length
2172
+ const start = Math.max(0, total - offset - limit)
2173
+ const end = total - offset
2174
+
2175
+ return unique.slice(start, end).map(({ _coreKey, _index, ...msg }) => msg)
1820
2176
  }
1821
2177
 
1822
2178
  /**
@@ -1825,14 +2181,18 @@ export class MostBoxEngine extends EventEmitter {
1825
2181
  * @param {string} content - 消息内容
1826
2182
  * @param {string} author - 作者 address
1827
2183
  * @param {string} authorName - 作者显示名
2184
+ * @param {object} [options.attachment] - 附件元数据
1828
2185
  * @returns {Promise<object>}
1829
2186
  */
1830
- async sendMessage(name, content, author, authorName) {
2187
+ async sendMessage(name, content, author, authorName, options = {}) {
1831
2188
  this.#ensureInitialized()
2189
+ this.#assertChannelMember(name, options.ownerAddress)
1832
2190
 
1833
- const core = this.#channelCores.get(name)
2191
+ const localKeyHex = this.#channelLocalCoreKey.get(name)
2192
+ const coresMap = this.#channelCores.get(name)
2193
+ const core = localKeyHex && coresMap ? coresMap.get(localKeyHex) : null
1834
2194
  if (!core) {
1835
- throw new Error('频道未初始化')
2195
+ throw new Error('频道未初始化或无可写 core')
1836
2196
  }
1837
2197
 
1838
2198
  if (!content || !content.trim()) {
@@ -1843,6 +2203,10 @@ export class MostBoxEngine extends EventEmitter {
1843
2203
  if (trimmed.length > MAX_MESSAGE_LENGTH) {
1844
2204
  throw new Error(`消息内容不能超过 ${MAX_MESSAGE_LENGTH} 字符`)
1845
2205
  }
2206
+ const attachment = normalizeChannelAttachment(options.attachment)
2207
+ if (attachment && trimmed !== attachment.link) {
2208
+ throw new ValidationError('attachment content must match link')
2209
+ }
1846
2210
 
1847
2211
  const message = {
1848
2212
  type: 'message',
@@ -1851,11 +2215,12 @@ export class MostBoxEngine extends EventEmitter {
1851
2215
  content: trimmed,
1852
2216
  timestamp: Date.now(),
1853
2217
  }
2218
+ if (attachment) {
2219
+ message.attachment = attachment
2220
+ }
1854
2221
 
1855
2222
  await core.append(message)
1856
2223
 
1857
- this.emit('channel:message', { channel: name, message })
1858
-
1859
2224
  return message
1860
2225
  }
1861
2226
 
@@ -1864,8 +2229,9 @@ export class MostBoxEngine extends EventEmitter {
1864
2229
  * @param {string} name - 频道名
1865
2230
  * @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
1866
2231
  */
1867
- getChannelPeers(name) {
2232
+ getChannelPeers(name, options = {}) {
1868
2233
  this.#ensureInitialized()
2234
+ this.#assertChannelMember(name, options.ownerAddress)
1869
2235
 
1870
2236
  const peers = this.#channelPeers.get(name)
1871
2237
  if (!peers) {
@@ -1922,27 +2288,26 @@ export class MostBoxEngine extends EventEmitter {
1922
2288
  }
1923
2289
  }
1924
2290
 
1925
- #getCidInfo(cid) {
1926
- try {
1927
- const parsedCid = CID.parse(cid)
1928
- const topic = b4a.from(parsedCid.multihash.digest)
1929
- if (topic.length !== 32) {
1930
- throw new ValidationError('CID digest must be 32 bytes')
1931
- }
1932
- const topicHex = b4a.toString(topic, 'hex')
1933
- return {
1934
- topic,
1935
- topicHex,
1936
- driveName: `drive-${topicHex}`,
1937
- }
1938
- } catch (err) {
1939
- if (err instanceof ValidationError) {
1940
- throw err
1941
- }
1942
- throw new ValidationError('Invalid CID format')
2291
+ #assertChannelMember(name, ownerAddress) {
2292
+ const normalizedOwner = normalizeOwnerAddress(ownerAddress)
2293
+ if (!normalizedOwner) return
2294
+
2295
+ const channel = this.#channels.find(c => c.name === name)
2296
+ if (!channel) {
2297
+ throw new Error('频道不存在')
2298
+ }
2299
+ if (
2300
+ !Array.isArray(channel.members) ||
2301
+ !channel.members.includes(normalizedOwner)
2302
+ ) {
2303
+ throw new PermissionError('未加入该频道')
1943
2304
  }
1944
2305
  }
1945
2306
 
2307
+ #getCidInfo(cid) {
2308
+ return getCidInfo(cid)
2309
+ }
2310
+
1946
2311
  #setSeedState(cid, patch = {}) {
1947
2312
  const previous = this.#seedStates.get(cid) || {}
1948
2313
  const next = {
@@ -1962,6 +2327,111 @@ export class MostBoxEngine extends EventEmitter {
1962
2327
  }
1963
2328
  }
1964
2329
 
2330
+ #getFileRuntimeStats(cid) {
2331
+ const state = this.#fileMonitors.get(cid)
2332
+ if (!state) {
2333
+ return {
2334
+ peerCount: 0,
2335
+ lastServedAt: null,
2336
+ totalServedBytes: 0,
2337
+ }
2338
+ }
2339
+
2340
+ return {
2341
+ peerCount: state.peerCount || 0,
2342
+ lastServedAt: state.lastServedAt || null,
2343
+ totalServedBytes: state.totalServedBytes || 0,
2344
+ }
2345
+ }
2346
+
2347
+ async #ensureFileMonitor(cid, drive = null) {
2348
+ const existing = this.#fileMonitors.get(cid)
2349
+ if (existing) return existing
2350
+
2351
+ const { driveName } = this.#getCidInfo(cid)
2352
+ const monitoredDrive = drive || (await this.#getOrCreateDrive(driveName))
2353
+ const monitor = monitoredDrive.monitor('/' + cid)
2354
+ const state = {
2355
+ cid,
2356
+ monitor,
2357
+ peerCount: 0,
2358
+ lastServedAt: null,
2359
+ totalServedBytes: 0,
2360
+ uploadBytes: 0,
2361
+ uploadBlocks: 0,
2362
+ lastMetricsEmittedAt: 0,
2363
+ cleanup: null,
2364
+ }
2365
+ this.#fileMonitors.set(cid, state)
2366
+
2367
+ const emitMetrics = (force = false) => {
2368
+ const now = Date.now()
2369
+ if (!force && now - state.lastMetricsEmittedAt < 1000) return
2370
+ state.lastMetricsEmittedAt = now
2371
+ this.emit('seed:metrics', {
2372
+ cid,
2373
+ ...this.#getFileRuntimeStats(cid),
2374
+ })
2375
+ }
2376
+
2377
+ const updatePeerCount = () => {
2378
+ const nextPeerCount = Number(monitor.peers) || 0
2379
+ if (nextPeerCount !== state.peerCount) {
2380
+ state.peerCount = nextPeerCount
2381
+ emitMetrics(true)
2382
+ }
2383
+ }
2384
+
2385
+ const updateTransferStats = () => {
2386
+ updatePeerCount()
2387
+ const uploadStats = monitor.uploadStats || {}
2388
+ const uploadBytes = Number(uploadStats.monitoringBytes) || 0
2389
+ const uploadBlocks = Number(uploadStats.blocks) || 0
2390
+ const servedMore =
2391
+ uploadBytes > state.uploadBytes || uploadBlocks > state.uploadBlocks
2392
+
2393
+ if (servedMore) {
2394
+ state.lastServedAt = new Date().toISOString()
2395
+ state.totalServedBytes = uploadBytes
2396
+ }
2397
+
2398
+ state.uploadBytes = uploadBytes
2399
+ state.uploadBlocks = uploadBlocks
2400
+ if (servedMore) emitMetrics()
2401
+ }
2402
+
2403
+ monitor.on('update', updateTransferStats)
2404
+ try {
2405
+ await monitor.ready()
2406
+ const blobs = monitor.blobs
2407
+ const onPeerUpdate = () => {
2408
+ updatePeerCount()
2409
+ }
2410
+ blobs?.core?.on('peer-add', onPeerUpdate)
2411
+ blobs?.core?.on('peer-remove', onPeerUpdate)
2412
+ state.cleanup = () => {
2413
+ blobs?.core?.off('peer-add', onPeerUpdate)
2414
+ blobs?.core?.off('peer-remove', onPeerUpdate)
2415
+ }
2416
+ updateTransferStats()
2417
+ } catch (err) {
2418
+ this.#fileMonitors.delete(cid)
2419
+ monitor.off('update', updateTransferStats)
2420
+ await monitor.close().catch(() => {})
2421
+ throw err
2422
+ }
2423
+
2424
+ return state
2425
+ }
2426
+
2427
+ async #closeFileMonitor(state) {
2428
+ if (!state) return
2429
+ try {
2430
+ state.cleanup?.()
2431
+ await state.monitor.close()
2432
+ } catch {}
2433
+ }
2434
+
1965
2435
  #resumeHoldingsInBackground() {
1966
2436
  if (this.#holdingResumeTask || this.#holdings.length === 0) {
1967
2437
  return
@@ -2028,7 +2498,6 @@ export class MostBoxEngine extends EventEmitter {
2028
2498
  topic: topicHex,
2029
2499
  driveName: record.driveName || driveName,
2030
2500
  source: record.source || 'manual',
2031
- temporary: record.temporary === true,
2032
2501
  }
2033
2502
  }
2034
2503
 
@@ -2049,6 +2518,12 @@ export class MostBoxEngine extends EventEmitter {
2049
2518
 
2050
2519
  this.#saveHoldingsMetadata()
2051
2520
  this.emit('holding:updated', next)
2521
+ this.#ensureFileMonitor(next.cid).catch(err => {
2522
+ this.#setSeedState(next.cid, {
2523
+ status: 'error',
2524
+ error: err.message,
2525
+ })
2526
+ })
2052
2527
  const seedState = this.#seedStates.get(next.cid)
2053
2528
  return {
2054
2529
  ...next,
@@ -2058,6 +2533,7 @@ export class MostBoxEngine extends EventEmitter {
2058
2533
  (this.#fileDiscoveries.has(next.cid) ? 'active' : 'queued'),
2059
2534
  seedError: seedState?.error,
2060
2535
  seedStatusUpdatedAt: seedState?.updatedAt,
2536
+ ...this.#getFileRuntimeStats(next.cid),
2061
2537
  }
2062
2538
  }
2063
2539
 
@@ -2068,11 +2544,15 @@ export class MostBoxEngine extends EventEmitter {
2068
2544
  this.#saveHoldingsMetadata()
2069
2545
  this.emit('holding:removed', { cid })
2070
2546
  }
2547
+ this.#closeFileMonitor(this.#fileMonitors.get(cid))
2548
+ this.#fileMonitors.delete(cid)
2071
2549
  this.#clearSeedState(cid)
2072
2550
  }
2073
2551
 
2074
2552
  async #joinCidTopicInternal(cid, options = {}) {
2075
2553
  const { topic, topicHex, driveName } = this.#getCidInfo(cid)
2554
+ const requestedServer = options.server !== false
2555
+ const requestedClient = options.client === true
2076
2556
  this.#setSeedState(cid, {
2077
2557
  status: 'joining',
2078
2558
  topic: topicHex,
@@ -2081,33 +2561,60 @@ export class MostBoxEngine extends EventEmitter {
2081
2561
  })
2082
2562
 
2083
2563
  try {
2084
- await this.#getOrCreateDrive(driveName)
2564
+ const drive = await this.#getOrCreateDrive(driveName)
2085
2565
 
2086
2566
  const existing = this.#fileDiscoveries.get(cid)
2087
2567
  if (existing) {
2088
- this.#setSeedState(cid, {
2089
- status: 'active',
2090
- topic: topicHex,
2091
- driveName,
2092
- error: undefined,
2093
- })
2094
- return {
2095
- cid,
2096
- topic: topicHex,
2097
- driveName,
2098
- joined: true,
2568
+ const nextServer = existing.server || requestedServer
2569
+ const nextClient = existing.client || requestedClient
2570
+ const needsRoleUpgrade =
2571
+ nextServer !== existing.server || nextClient !== existing.client
2572
+
2573
+ if (!needsRoleUpgrade) {
2574
+ if (this.#holdings.some(holding => holding.cid === cid)) {
2575
+ this.#ensureFileMonitor(cid, drive).catch(err => {
2576
+ this.#setSeedState(cid, {
2577
+ status: 'error',
2578
+ error: err.message,
2579
+ })
2580
+ })
2581
+ }
2582
+ this.#setSeedState(cid, {
2583
+ status: 'active',
2584
+ topic: topicHex,
2585
+ driveName,
2586
+ error: undefined,
2587
+ })
2588
+ return {
2589
+ cid,
2590
+ topic: topicHex,
2591
+ driveName,
2592
+ joined: true,
2593
+ }
2099
2594
  }
2595
+
2596
+ await this.#swarm.leave(topic).catch(err => {
2597
+ console.warn(
2598
+ `[MostBox] Failed to upgrade CID topic role for ${cid}:`,
2599
+ err.message
2600
+ )
2601
+ })
2602
+ this.#fileDiscoveries.delete(cid)
2100
2603
  }
2101
2604
 
2605
+ const server = existing?.server || requestedServer
2606
+ const client = existing?.client || requestedClient
2102
2607
  const discovery = this.#swarm.join(topic, {
2103
- server: options.server !== false,
2104
- client: options.client === true,
2608
+ server,
2609
+ client,
2105
2610
  })
2106
2611
 
2107
2612
  this.#fileDiscoveries.set(cid, {
2108
2613
  discovery,
2109
2614
  topic: topicHex,
2110
2615
  driveName,
2616
+ server,
2617
+ client,
2111
2618
  })
2112
2619
  this.#setSeedState(cid, {
2113
2620
  status: 'active',
@@ -2115,6 +2622,14 @@ export class MostBoxEngine extends EventEmitter {
2115
2622
  driveName,
2116
2623
  error: undefined,
2117
2624
  })
2625
+ if (this.#holdings.some(holding => holding.cid === cid)) {
2626
+ this.#ensureFileMonitor(cid, drive).catch(err => {
2627
+ this.#setSeedState(cid, {
2628
+ status: 'error',
2629
+ error: err.message,
2630
+ })
2631
+ })
2632
+ }
2118
2633
  this.emit('file:topic:joined', { cid, topic: topicHex, driveName })
2119
2634
 
2120
2635
  return {
@@ -2158,19 +2673,44 @@ export class MostBoxEngine extends EventEmitter {
2158
2673
  return null
2159
2674
  }
2160
2675
 
2161
- if (this.#swarm) {
2162
- this.#swarm.leave(drive.discoveryKey).catch(err => {
2163
- console.warn(
2164
- `[MostBox] Failed to leave drive discovery ${driveName}:`,
2165
- err.message
2166
- )
2167
- })
2168
- }
2169
2676
  await drive.close()
2170
2677
  this.#drives.delete(driveName)
2171
2678
  return drive
2172
2679
  }
2173
2680
 
2681
+ #recordMatchesOwner(record, ownerAddress) {
2682
+ const normalizedOwner = normalizeOwnerAddress(ownerAddress)
2683
+ if (!normalizedOwner) return !record.ownerAddress
2684
+ return normalizeOwnerAddress(record.ownerAddress) === normalizedOwner
2685
+ }
2686
+
2687
+ #hasPublishedReference(cid) {
2688
+ return this.#publishedFiles.some(file => file.cid === cid)
2689
+ }
2690
+
2691
+ #hasAnyUserReference(cid) {
2692
+ return (
2693
+ this.#publishedFiles.some(file => file.cid === cid) ||
2694
+ this.#trashFiles.some(file => file.cid === cid)
2695
+ )
2696
+ }
2697
+
2698
+ #getUsedBytes() {
2699
+ return this.#holdings.reduce((sum, h) => sum + (h.size || 0), 0)
2700
+ }
2701
+
2702
+ #checkCapacity(additionalBytes) {
2703
+ const used = this.#getUsedBytes()
2704
+ const capacity = this.#options.capacityBytes
2705
+ if (used + additionalBytes > capacity) {
2706
+ const usedGB = (used / (1024 * 1024 * 1024)).toFixed(2)
2707
+ const capacityGB = (capacity / (1024 * 1024 * 1024)).toFixed(2)
2708
+ throw new StorageCapacityError(
2709
+ `Storage capacity exceeded: used ${usedGB} GB, capacity ${capacityGB} GB`
2710
+ )
2711
+ }
2712
+ }
2713
+
2174
2714
  async #getOrCreateDrive(name, _options = { server: true, client: false }) {
2175
2715
  if (this.#drives.has(name)) return this.#drives.get(name)
2176
2716
  if (this.#drivePromises.has(name)) return this.#drivePromises.get(name)
@@ -2193,11 +2733,6 @@ export class MostBoxEngine extends EventEmitter {
2193
2733
  }
2194
2734
 
2195
2735
  async #syncDrive(drive, timeout = DRIVE_SYNC_TIMEOUT) {
2196
- const done = drive.findingPeers()
2197
- this.#swarm
2198
- .join(drive.discoveryKey, { server: true, client: true })
2199
- .flushed()
2200
- .then(done, done)
2201
2736
  try {
2202
2737
  const updated = await Promise.race([
2203
2738
  drive.update(),
@@ -2384,15 +2919,57 @@ export class MostBoxEngine extends EventEmitter {
2384
2919
  })
2385
2920
  }
2386
2921
 
2922
+ async #openRemoteChannelCore(channelName, coreKeyHex) {
2923
+ const coresMap = this.#channelCores.get(channelName)
2924
+ if (!coresMap) return
2925
+ if (coresMap.has(coreKeyHex)) return
2926
+
2927
+ try {
2928
+ const ns = this.#store.namespace(`channel-${channelName}`)
2929
+ const core = ns.get({
2930
+ key: b4a.from(coreKeyHex, 'hex'),
2931
+ valueEncoding: 'json',
2932
+ })
2933
+ await core.ready()
2934
+ const normalizedCoreKey = b4a.toString(core.key, 'hex')
2935
+ coresMap.set(normalizedCoreKey, core)
2936
+ this.#setupChannelAppendListener(core, channelName)
2937
+ const channel = this.#channels.find(c => c.name === channelName)
2938
+ if (channel && normalizedCoreKey !== channel.coreKey) {
2939
+ if (!Array.isArray(channel.remoteCoreKeys)) {
2940
+ channel.remoteCoreKeys = []
2941
+ }
2942
+ if (!channel.remoteCoreKeys.includes(normalizedCoreKey)) {
2943
+ channel.remoteCoreKeys.push(normalizedCoreKey)
2944
+ this.#saveChannelsMetadata()
2945
+ }
2946
+ }
2947
+ console.log(
2948
+ `[MostBox] Opened remote channel core ${normalizedCoreKey.slice(0, 8)}... for ${channelName}`
2949
+ )
2950
+ } catch (err) {
2951
+ console.warn(
2952
+ `[MostBox] Failed to open remote channel core for ${channelName}:`,
2953
+ err.message
2954
+ )
2955
+ }
2956
+ }
2957
+
2387
2958
  async #handleChannelConnection(conn) {
2388
2959
  const stream = conn
2389
2960
  let connectedPeerId = null
2390
2961
 
2962
+ const coreKeys = {}
2963
+ for (const [name, localKeyHex] of this.#channelLocalCoreKey) {
2964
+ coreKeys[name] = localKeyHex
2965
+ }
2966
+
2391
2967
  const helloMessage = JSON.stringify({
2392
2968
  type: 'channel-hello',
2393
2969
  peerId: this.getNodeId(),
2394
2970
  authorName: this.getNodeId().slice(0, 4),
2395
2971
  channels: this.#channels.map(c => c.name),
2972
+ coreKeys,
2396
2973
  })
2397
2974
 
2398
2975
  try {
@@ -2417,6 +2994,17 @@ export class MostBoxEngine extends EventEmitter {
2417
2994
  })
2418
2995
  }
2419
2996
  }
2997
+
2998
+ if (msg.coreKeys && typeof msg.coreKeys === 'object') {
2999
+ for (const [channelName, coreKeyHex] of Object.entries(
3000
+ msg.coreKeys
3001
+ )) {
3002
+ if (this.#channelCores.has(channelName) && coreKeyHex) {
3003
+ await this.#openRemoteChannelCore(channelName, coreKeyHex)
3004
+ }
3005
+ }
3006
+ }
3007
+
2420
3008
  this.emit('channel:peer:online', {
2421
3009
  peerId: msg.peerId,
2422
3010
  authorName: msg.authorName,