most-box 0.1.1 → 0.1.3

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 (231) hide show
  1. package/README.md +39 -19
  2. package/electron/afterPack.cjs +87 -0
  3. package/electron/main.js +156 -8
  4. package/electron/preload.js +6 -0
  5. package/electron/updateChecker.js +97 -0
  6. package/electron/updateChecker.test.js +147 -0
  7. package/out/404/index.html +2 -2
  8. package/out/404.html +2 -2
  9. package/out/__next.__PAGE__.txt +6 -6
  10. package/out/__next._full.txt +16 -16
  11. package/out/__next._head.txt +3 -3
  12. package/out/__next._index.txt +8 -8
  13. package/out/__next._tree.txt +5 -5
  14. package/out/_next/static/chunks/04mo7rr..0_1q.js +1 -0
  15. package/out/_next/static/chunks/06rf3qq5ggs6v.js +1 -0
  16. package/out/_next/static/chunks/07td.jq7xff84.css +1 -0
  17. package/out/_next/static/chunks/0_0oph_z1az14.js +1 -0
  18. package/out/_next/static/chunks/{0qou.u2e2dy48.css → 0adx~d-j05c9d.css} +2 -2
  19. package/out/_next/static/chunks/0cl7d~7abnk_p.css +1 -0
  20. package/out/_next/static/chunks/0d306t1wvjpdx.js +1 -0
  21. package/out/_next/static/chunks/0g_a~e050bgzg.css +1 -0
  22. package/out/_next/static/chunks/{0o6lrkxy4jwag.js → 0gcsdf57gcm6h.js} +1 -1
  23. package/out/_next/static/chunks/0hpev4am9jpmu.css +1 -0
  24. package/out/_next/static/chunks/0m_5nb6x8qy._.js +1 -0
  25. package/out/_next/static/chunks/0n.ayxmsar6e5.js +1 -0
  26. package/out/_next/static/chunks/{0usvo~vu7r8np.js → 0o9ce4cyf76by.js} +1 -1
  27. package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
  28. package/out/_next/static/chunks/{0o98f1yq..o.8.js → 0pt.5cg1t09qs.js} +1 -1
  29. package/out/_next/static/chunks/0qgx9t4jx16ua.css +1 -0
  30. package/out/_next/static/chunks/0s~g.l~x049o2.js +1 -0
  31. package/out/_next/static/chunks/0ukyg~tkm~h2m.css +1 -0
  32. package/out/_next/static/chunks/0voe1.ttrh84k.css +1 -0
  33. package/out/_next/static/chunks/0wtf0xsiicxx6.js +1 -0
  34. package/out/_next/static/chunks/0x.ky97owcxxs.js +1 -0
  35. package/out/_next/static/chunks/0xdwau5k2augv.css +4 -0
  36. package/out/_next/static/chunks/0ysj5b94vu4ri.js +1 -0
  37. package/out/_next/static/chunks/12nr19.nnn6s3.js +5 -0
  38. package/out/_next/static/chunks/{0qub_r0x_r-e9.css → 12pep-2t-qg4n.css} +1 -1
  39. package/out/_next/static/chunks/14_inksek_rth.js +2 -0
  40. package/out/_next/static/chunks/153-sz7s.qml2.js +1 -0
  41. package/out/_next/static/chunks/17cwkb2yn_akx.js +1 -0
  42. package/out/_next/static/chunks/184hxsuf-5c84.js +1 -0
  43. package/out/_next/static/chunks/{turbopack-0xs6mybc~5t_3.js → turbopack-0xta0kqwzkf28.js} +1 -1
  44. package/out/_not-found/__next._full.txt +13 -13
  45. package/out/_not-found/__next._head.txt +3 -3
  46. package/out/_not-found/__next._index.txt +8 -8
  47. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  48. package/out/_not-found/__next._not-found.txt +3 -3
  49. package/out/_not-found/__next._tree.txt +3 -3
  50. package/out/_not-found/index.html +2 -2
  51. package/out/_not-found/index.txt +13 -13
  52. package/out/admin/__next._full.txt +15 -15
  53. package/out/admin/__next._head.txt +3 -3
  54. package/out/admin/__next._index.txt +8 -8
  55. package/out/admin/__next._tree.txt +4 -4
  56. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  57. package/out/admin/__next.admin.txt +4 -4
  58. package/out/admin/index.html +2 -2
  59. package/out/admin/index.txt +15 -15
  60. package/out/app/__next._full.txt +14 -14
  61. package/out/app/__next._head.txt +3 -3
  62. package/out/app/__next._index.txt +8 -8
  63. package/out/app/__next._tree.txt +3 -3
  64. package/out/app/__next.app.__PAGE__.txt +4 -4
  65. package/out/app/__next.app.txt +3 -3
  66. package/out/app/index.html +2 -2
  67. package/out/app/index.txt +14 -14
  68. package/out/chat/__next._full.txt +15 -15
  69. package/out/chat/__next._head.txt +3 -3
  70. package/out/chat/__next._index.txt +8 -8
  71. package/out/chat/__next._tree.txt +4 -4
  72. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  73. package/out/chat/__next.chat.txt +4 -4
  74. package/out/chat/index.html +2 -2
  75. package/out/chat/index.txt +15 -15
  76. package/out/chat/join/__next._full.txt +25 -0
  77. package/out/chat/join/__next._head.txt +5 -0
  78. package/out/{changelog → chat/join}/__next._index.txt +8 -8
  79. package/out/chat/join/__next._tree.txt +5 -0
  80. package/out/chat/join/__next.chat.join.__PAGE__.txt +9 -0
  81. package/out/chat/join/__next.chat.join.txt +5 -0
  82. package/out/chat/join/__next.chat.txt +5 -0
  83. package/out/chat/join/index.html +15 -0
  84. package/out/chat/join/index.txt +25 -0
  85. package/out/download/__next._full.txt +37 -33
  86. package/out/download/__next._head.txt +3 -3
  87. package/out/download/__next._index.txt +8 -8
  88. package/out/download/__next._tree.txt +5 -5
  89. package/out/download/__next.download.__PAGE__.txt +9 -14
  90. package/out/download/__next.download.txt +3 -3
  91. package/out/download/index.html +2 -2
  92. package/out/download/index.txt +37 -33
  93. package/out/favicon.ico +0 -0
  94. package/out/gandengyan/__next._full.txt +25 -0
  95. package/out/{changelog → gandengyan}/__next._head.txt +3 -3
  96. package/out/{docs → gandengyan}/__next._index.txt +8 -8
  97. package/out/gandengyan/__next._tree.txt +5 -0
  98. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +10 -0
  99. package/out/gandengyan/__next.gandengyan.txt +5 -0
  100. package/out/gandengyan/index.html +15 -0
  101. package/out/gandengyan/index.txt +25 -0
  102. package/out/index.html +2 -2
  103. package/out/index.txt +16 -16
  104. package/out/note/__next._full.txt +14 -14
  105. package/out/note/__next._head.txt +3 -3
  106. package/out/note/__next._index.txt +8 -8
  107. package/out/note/__next._tree.txt +3 -3
  108. package/out/note/__next.note.__PAGE__.txt +4 -4
  109. package/out/note/__next.note.txt +3 -3
  110. package/out/note/index.html +2 -2
  111. package/out/note/index.txt +14 -14
  112. package/out/ping/__next._full.txt +16 -16
  113. package/out/ping/__next._head.txt +3 -3
  114. package/out/ping/__next._index.txt +8 -8
  115. package/out/ping/__next._tree.txt +5 -5
  116. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  117. package/out/ping/__next.ping.txt +4 -4
  118. package/out/ping/index.html +2 -2
  119. package/out/ping/index.txt +16 -16
  120. package/out/web3/__next._full.txt +15 -15
  121. package/out/web3/__next._head.txt +3 -3
  122. package/out/web3/__next._index.txt +8 -8
  123. package/out/web3/__next._tree.txt +4 -4
  124. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  125. package/out/web3/__next.web3.txt +4 -4
  126. package/out/web3/ed25519/__next._full.txt +13 -13
  127. package/out/web3/ed25519/__next._head.txt +3 -3
  128. package/out/web3/ed25519/__next._index.txt +8 -8
  129. package/out/web3/ed25519/__next._tree.txt +4 -4
  130. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  131. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  132. package/out/web3/ed25519/__next.web3.txt +4 -4
  133. package/out/web3/ed25519/index.html +1 -1
  134. package/out/web3/ed25519/index.txt +13 -13
  135. package/out/web3/index.html +2 -2
  136. package/out/web3/index.txt +15 -15
  137. package/out/web3/tools/__next._full.txt +13 -13
  138. package/out/web3/tools/__next._head.txt +3 -3
  139. package/out/web3/tools/__next._index.txt +8 -8
  140. package/out/web3/tools/__next._tree.txt +4 -4
  141. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  142. package/out/web3/tools/__next.web3.tools.txt +3 -3
  143. package/out/web3/tools/__next.web3.txt +4 -4
  144. package/out/web3/tools/index.html +1 -1
  145. package/out/web3/tools/index.txt +13 -13
  146. package/package.json +44 -35
  147. package/public/favicon.ico +0 -0
  148. package/server/index.js +142 -1304
  149. package/server/src/config.js +1 -1
  150. package/server/src/core/channelAttachment.js +68 -0
  151. package/server/src/core/cid.js +2 -88
  152. package/server/src/core/cidTopic.js +29 -0
  153. package/server/src/core/mostLink.js +88 -0
  154. package/server/src/games/gandengyan.js +675 -0
  155. package/server/src/http/access.js +127 -0
  156. package/server/src/http/app.js +1102 -0
  157. package/server/src/http/errors.js +35 -0
  158. package/server/src/http/nodeLogs.js +53 -0
  159. package/server/src/http/nodeStatus.js +146 -0
  160. package/server/src/http/staticFiles.js +84 -0
  161. package/server/src/http/uploads.js +114 -0
  162. package/server/src/index.js +799 -211
  163. package/server/src/node/config.js +38 -6
  164. package/server/src/utils/api.js +305 -14
  165. package/server/src/utils/auth.js +63 -0
  166. package/server/src/utils/dateTime.js +30 -0
  167. package/server/src/utils/downloadMessages.js +89 -0
  168. package/server/src/utils/errors.js +7 -0
  169. package/server/src/utils/mostWallet.js +151 -0
  170. package/server/src/utils/mp.js +2 -26
  171. package/server/src/utils/noteBackup.js +2 -5
  172. package/server/src/utils/noteUtils.js +11 -3
  173. package/server/src/utils/userIdentity.js +0 -1
  174. package/out/_next/static/chunks/00-u5nq76f0.j.js +0 -1
  175. package/out/_next/static/chunks/00fm8lijienf1.js +0 -1
  176. package/out/_next/static/chunks/00o9ht.f2qm00.css +0 -4
  177. package/out/_next/static/chunks/00zi-erhjrny2.js +0 -2
  178. package/out/_next/static/chunks/084xf0edl9sfo.js +0 -1
  179. package/out/_next/static/chunks/09f1gfke9m5wg.css +0 -1
  180. package/out/_next/static/chunks/09xyi6fpro_d-.css +0 -1
  181. package/out/_next/static/chunks/0_npg_pcoywti.js +0 -5
  182. package/out/_next/static/chunks/0_r_mk1~6bosc.js +0 -1
  183. package/out/_next/static/chunks/0arm0a6adt7cc.css +0 -1
  184. package/out/_next/static/chunks/0c9j3eq_14vv2.css +0 -1
  185. package/out/_next/static/chunks/0d4bueddmcnca.js +0 -1
  186. package/out/_next/static/chunks/0gtwvy1z9ksa7.css +0 -1
  187. package/out/_next/static/chunks/0ho~log~~-jwp.css +0 -1
  188. package/out/_next/static/chunks/0j27tcmtt4ly7.js +0 -1
  189. package/out/_next/static/chunks/0j3v4mq67wtnh.js +0 -1
  190. package/out/_next/static/chunks/0lkmf5ry.s_7w.js +0 -1
  191. package/out/_next/static/chunks/0p486m03-zfoi.js +0 -1
  192. package/out/_next/static/chunks/0r1~k82nji8sf.js +0 -1
  193. package/out/_next/static/chunks/0v7qp4hv-_._r.js +0 -1
  194. package/out/_next/static/chunks/0wuwlgcn6gxqt.js +0 -1
  195. package/out/_next/static/chunks/0xl5_avhu._i8.js +0 -1
  196. package/out/_next/static/chunks/10kvl8vj_plm-.js +0 -1
  197. package/out/_next/static/chunks/16m27azcs4k6w.js +0 -1
  198. package/out/changelog/__next._full.txt +0 -25
  199. package/out/changelog/__next._tree.txt +0 -5
  200. package/out/changelog/__next.changelog.__PAGE__.txt +0 -10
  201. package/out/changelog/__next.changelog.txt +0 -5
  202. package/out/changelog/index.html +0 -15
  203. package/out/changelog/index.txt +0 -25
  204. package/out/docs/__next._full.txt +0 -25
  205. package/out/docs/__next._head.txt +0 -5
  206. package/out/docs/__next._tree.txt +0 -5
  207. package/out/docs/__next.docs.__PAGE__.txt +0 -10
  208. package/out/docs/__next.docs.txt +0 -5
  209. package/out/docs/getting-started/__next._full.txt +0 -25
  210. package/out/docs/getting-started/__next._head.txt +0 -5
  211. package/out/docs/getting-started/__next._index.txt +0 -9
  212. package/out/docs/getting-started/__next._tree.txt +0 -5
  213. package/out/docs/getting-started/__next.docs.getting-started.__PAGE__.txt +0 -10
  214. package/out/docs/getting-started/__next.docs.getting-started.txt +0 -5
  215. package/out/docs/getting-started/__next.docs.txt +0 -5
  216. package/out/docs/getting-started/index.html +0 -15
  217. package/out/docs/getting-started/index.txt +0 -25
  218. package/out/docs/index.html +0 -15
  219. package/out/docs/index.txt +0 -25
  220. package/out/note/edit/__next._full.txt +0 -24
  221. package/out/note/edit/__next._head.txt +0 -5
  222. package/out/note/edit/__next._index.txt +0 -9
  223. package/out/note/edit/__next._tree.txt +0 -4
  224. package/out/note/edit/__next.note.edit.__PAGE__.txt +0 -9
  225. package/out/note/edit/__next.note.edit.txt +0 -5
  226. package/out/note/edit/__next.note.txt +0 -5
  227. package/out/note/edit/index.html +0 -15
  228. package/out/note/edit/index.txt +0 -24
  229. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_buildManifest.js +0 -0
  230. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_clientMiddlewareManifest.js +0 -0
  231. /package/out/_next/static/{sIuUKxnnGU7K9Tu9UDKE8 → aPEZ4zaaR5W3WpSZ0dFsa}/_ssgManifest.js +0 -0
@@ -6,11 +6,11 @@ import { MAX_FILE_SIZE } from '../config.js'
6
6
  const DEFAULT_CONFIG_DIR_NAME = '.most-box'
7
7
  const DEFAULT_DATA_DIR_NAME = 'most-data'
8
8
  const DEFAULT_CAPACITY_BYTES = 100 * 1024 * 1024 * 1024
9
+ export const DEFAULT_NODE_PORT = 1976
10
+ export const DEFAULT_NODE_HOST = '127.0.0.1'
9
11
 
10
12
  export function getDefaultConfigDir() {
11
- return process.env.MOSTBOX_CONFIG_DIR
12
- ? path.resolve(process.env.MOSTBOX_CONFIG_DIR)
13
- : path.join(os.homedir(), DEFAULT_CONFIG_DIR_NAME)
13
+ return path.join(os.homedir(), DEFAULT_CONFIG_DIR_NAME)
14
14
  }
15
15
 
16
16
  export function getDefaultDataPath() {
@@ -20,11 +20,21 @@ export function getDefaultDataPath() {
20
20
  export function getDefaultNodeConfig() {
21
21
  return {
22
22
  dataPath: '',
23
+ host: DEFAULT_NODE_HOST,
24
+ port: DEFAULT_NODE_PORT,
23
25
  capacityBytes: DEFAULT_CAPACITY_BYTES,
24
26
  maxFileSizeBytes: MAX_FILE_SIZE,
27
+ remoteInvites: [],
25
28
  }
26
29
  }
27
30
 
31
+ export function normalizeRemoteInvites(value = []) {
32
+ const items = Array.isArray(value) ? value : String(value || '').split(',')
33
+ return Array.from(
34
+ new Set(items.map(item => String(item || '').trim()).filter(Boolean))
35
+ )
36
+ }
37
+
28
38
  export function normalizeNodeConfig(raw = {}) {
29
39
  const defaults = getDefaultNodeConfig()
30
40
  const rawNode = raw.node && typeof raw.node === 'object' ? raw.node : {}
@@ -37,13 +47,24 @@ export function normalizeNodeConfig(raw = {}) {
37
47
  rawNode.maxFileSizeBytes ?? raw.maxFileSizeBytes,
38
48
  defaults.maxFileSizeBytes
39
49
  )
50
+ const remoteInvites = normalizeRemoteInvites(
51
+ rawNode.remoteInvites ?? raw.remoteInvites ?? defaults.remoteInvites
52
+ )
53
+ const host = normalizeHost(rawNode.host ?? raw.host, defaults.host)
54
+ const port = normalizePositiveInteger(
55
+ rawNode.port ?? raw.port,
56
+ defaults.port
57
+ )
40
58
  return {
41
59
  dataPath:
42
60
  typeof raw.dataPath === 'string'
43
61
  ? raw.dataPath.trim()
44
62
  : defaults.dataPath,
63
+ host,
64
+ port,
45
65
  capacityBytes,
46
66
  maxFileSizeBytes,
67
+ remoteInvites,
47
68
  }
48
69
  }
49
70
 
@@ -82,9 +103,6 @@ export function createNodeConfigStore(configDir = getDefaultConfigDir()) {
82
103
  }
83
104
 
84
105
  function getDataPath() {
85
- if (process.env.MOSTBOX_DATA_PATH) {
86
- return process.env.MOSTBOX_DATA_PATH
87
- }
88
106
  return getNodeConfig().dataPath || getDefaultDataPath()
89
107
  }
90
108
 
@@ -97,6 +115,8 @@ export function createNodeConfigStore(configDir = getDefaultConfigDir()) {
97
115
  patch.dataPath === undefined ? current.dataPath : patch.dataPath,
98
116
  node: {
99
117
  ...(raw.node && typeof raw.node === 'object' ? raw.node : {}),
118
+ host: patch.host === undefined ? current.host : patch.host,
119
+ port: patch.port === undefined ? current.port : patch.port,
100
120
  capacityBytes:
101
121
  patch.capacityBytes === undefined
102
122
  ? current.capacityBytes
@@ -105,14 +125,21 @@ export function createNodeConfigStore(configDir = getDefaultConfigDir()) {
105
125
  patch.maxFileSizeBytes === undefined
106
126
  ? current.maxFileSizeBytes
107
127
  : patch.maxFileSizeBytes,
128
+ remoteInvites:
129
+ patch.remoteInvites === undefined
130
+ ? current.remoteInvites
131
+ : patch.remoteInvites,
108
132
  },
109
133
  })
110
134
 
111
135
  const saved = {
112
136
  dataPath: next.dataPath,
113
137
  node: {
138
+ host: next.host,
139
+ port: next.port,
114
140
  capacityBytes: next.capacityBytes,
115
141
  maxFileSizeBytes: next.maxFileSizeBytes,
142
+ remoteInvites: next.remoteInvites,
116
143
  updatedAt: new Date().toISOString(),
117
144
  },
118
145
  }
@@ -157,3 +184,8 @@ function normalizePositiveInteger(value, fallback) {
157
184
  }
158
185
  return Math.floor(parsed)
159
186
  }
187
+
188
+ function normalizeHost(value, fallback) {
189
+ const host = String(value || '').trim()
190
+ return host || fallback
191
+ }
@@ -1,15 +1,14 @@
1
1
  import ky from 'ky'
2
+ import { buildAuthHeaders, normalizeAuthPath } from './auth.js'
2
3
 
3
4
  const STORAGE_KEY = 'mostbox_backend_url'
5
+ const INVITE_STORAGE_KEY = 'mostbox_backend_invite'
4
6
  const LOCALHOST_BACKEND_URL = 'http://localhost:1976'
5
7
 
6
8
  function isLocalFrontendOrigin() {
7
9
  if (typeof window === 'undefined') return false
8
10
 
9
- return (
10
- ['localhost', '127.0.0.1'].includes(window.location.hostname) &&
11
- window.location.port === '3000'
12
- )
11
+ return ['localhost', '127.0.0.1'].includes(window.location.hostname)
13
12
  }
14
13
 
15
14
  function getDefaultBackendUrl() {
@@ -21,14 +20,136 @@ function getBackendUrl() {
21
20
  return localStorage.getItem(STORAGE_KEY) || getDefaultBackendUrl()
22
21
  }
23
22
 
23
+ function getConfiguredBackendUrl() {
24
+ if (typeof window === 'undefined') return ''
25
+ return localStorage.getItem(STORAGE_KEY) || ''
26
+ }
27
+
28
+ function getRemoteBackendUrl() {
29
+ const configured = getConfiguredBackendUrl()
30
+ if (!configured) return ''
31
+ try {
32
+ const { hostname } = new URL(configured)
33
+ return ['localhost', '127.0.0.1', '::1'].includes(hostname)
34
+ ? ''
35
+ : configured
36
+ } catch {
37
+ return configured
38
+ }
39
+ }
40
+
41
+ function getBackendInvite() {
42
+ if (typeof window === 'undefined') return ''
43
+ return localStorage.getItem(INVITE_STORAGE_KEY) || ''
44
+ }
45
+
46
+ function normalizeBackendUrl(url) {
47
+ return (url || '').trim().replace(/\/+$/, '')
48
+ }
49
+
50
+ function isLocalBackendUrl(url) {
51
+ const value = normalizeBackendUrl(url)
52
+ if (!value) return false
53
+ try {
54
+ const { hostname } = new URL(value)
55
+ const normalized = hostname.toLowerCase()
56
+ return (
57
+ normalized === 'localhost' ||
58
+ normalized === '::1' ||
59
+ normalized === '[::1]' ||
60
+ normalized === '127.0.0.1' ||
61
+ normalized.startsWith('127.')
62
+ )
63
+ } catch {
64
+ return false
65
+ }
66
+ }
67
+
68
+ function shouldAttachBackendInvite(url = getBackendUrl()) {
69
+ return Boolean(getBackendInvite()) && !isLocalBackendUrl(url)
70
+ }
71
+
72
+ function getStoredIdentity() {
73
+ if (typeof window === 'undefined') return null
74
+ try {
75
+ const raw = localStorage.getItem('mostbox_identity')
76
+ return raw ? JSON.parse(raw) : null
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
24
82
  function normalizePath(path) {
25
83
  return path.startsWith('/') ? path : `/${path}`
26
84
  }
27
85
 
86
+ function getBackendAuthPath(url) {
87
+ const requestPath = normalizeAuthPath(url)
88
+ const backendUrl = getBackendUrl()
89
+ if (!backendUrl) return requestPath
90
+
91
+ try {
92
+ const basePath = new URL(backendUrl).pathname.replace(/\/+$/, '')
93
+ if (!basePath || basePath === '/') return requestPath
94
+ if (requestPath === basePath) return '/'
95
+ if (requestPath.startsWith(`${basePath}/`)) {
96
+ return requestPath.slice(basePath.length)
97
+ }
98
+ } catch {}
99
+
100
+ return requestPath
101
+ }
102
+
103
+ function buildWebSocketUrl(base, wsPath = '/ws') {
104
+ const url = new URL(base)
105
+ const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
106
+ const basePath = url.pathname.replace(/\/+$/, '')
107
+ return `${wsProtocol}//${url.host}${basePath}${normalizePath(wsPath)}`
108
+ }
109
+
28
110
  function createApiInstance() {
29
- const url = getBackendUrl()
30
- return ky.create({
31
- prefix: url,
111
+ const client = ky.create({
112
+ hooks: {
113
+ beforeRequest: [
114
+ async ({ request }) => {
115
+ const headers = new Headers(request.headers || {})
116
+ const invite = getBackendInvite()
117
+ if (invite && shouldAttachBackendInvite(request.url)) {
118
+ headers.set('x-mostbox-invite', invite)
119
+ }
120
+
121
+ const identity = getStoredIdentity()
122
+ if (identity?.danger) {
123
+ try {
124
+ const authHeaders = await buildAuthHeaders(
125
+ identity,
126
+ request.method,
127
+ getBackendAuthPath(request.url)
128
+ )
129
+ for (const [key, value] of Object.entries(authHeaders)) {
130
+ headers.set(key, value)
131
+ }
132
+ } catch {
133
+ // Ignore invalid legacy identity data for public/backend probes.
134
+ }
135
+ }
136
+ return new Request(request, { headers })
137
+ },
138
+ ],
139
+ },
140
+ })
141
+
142
+ return new Proxy(client, {
143
+ get(target, prop, receiver) {
144
+ const value = Reflect.get(target, prop, receiver)
145
+ if (!['get', 'post', 'put', 'patch', 'delete', 'head'].includes(prop)) {
146
+ return value
147
+ }
148
+ return (input, options) => {
149
+ const nextInput = typeof input === 'string' ? getApiUrl(input) : input
150
+ return value.call(target, nextInput, options)
151
+ }
152
+ },
32
153
  })
33
154
  }
34
155
 
@@ -71,7 +192,7 @@ export async function getApiErrorMessage(err, fallback = '请求失败') {
71
192
  err && typeof err === 'object' && 'name' in err ? String(err.name) : ''
72
193
  if (errorName === 'TimeoutError') return '请求超时,请稍后重试'
73
194
 
74
- if (!data.status && err instanceof Error && err.message) {
195
+ if (err instanceof Error && err.message) {
75
196
  return err.message
76
197
  }
77
198
 
@@ -79,7 +200,7 @@ export async function getApiErrorMessage(err, fallback = '请求失败') {
79
200
  }
80
201
 
81
202
  export function setBackendUrl(url) {
82
- const cleaned = (url || '').trim().replace(/\/+$/, '')
203
+ const cleaned = normalizeBackendUrl(url)
83
204
  if (cleaned) {
84
205
  localStorage.setItem(STORAGE_KEY, cleaned)
85
206
  } else {
@@ -88,37 +209,207 @@ export function setBackendUrl(url) {
88
209
  api = createApiInstance()
89
210
  }
90
211
 
212
+ export function setBackendInvite(invite) {
213
+ const cleaned = (invite || '').trim()
214
+ if (cleaned) {
215
+ localStorage.setItem(INVITE_STORAGE_KEY, cleaned)
216
+ } else {
217
+ localStorage.removeItem(INVITE_STORAGE_KEY)
218
+ }
219
+ api = createApiInstance()
220
+ }
221
+
222
+ export function configureBackend({ url, invite }) {
223
+ setBackendUrl(url)
224
+ setBackendInvite(invite)
225
+ }
226
+
227
+ export function clearBackendConnection() {
228
+ setBackendUrl('')
229
+ setBackendInvite('')
230
+ }
231
+
91
232
  export function getBackendUrlExport() {
92
233
  return getBackendUrl()
93
234
  }
94
235
 
236
+ export function getRemoteBackendUrlExport() {
237
+ return getRemoteBackendUrl()
238
+ }
239
+
240
+ export function getBackendInviteExport() {
241
+ return getBackendInvite()
242
+ }
243
+
95
244
  export function getApiUrl(path) {
96
245
  const url = getBackendUrl()
97
246
  return `${url}${normalizePath(path)}`
98
247
  }
99
248
 
249
+ export async function getApiRequestHeaders(method = 'GET', path = '/') {
250
+ /** @type {Record<string, string>} */
251
+ const headers = {}
252
+ const invite = getBackendInvite()
253
+ if (invite && shouldAttachBackendInvite()) {
254
+ headers['x-mostbox-invite'] = invite
255
+ }
256
+ try {
257
+ Object.assign(
258
+ headers,
259
+ await buildAuthHeaders(
260
+ getStoredIdentity(),
261
+ method,
262
+ normalizeAuthPath(path)
263
+ )
264
+ )
265
+ } catch {
266
+ // Callers that require auth will receive the server's 401 response.
267
+ }
268
+ return headers
269
+ }
270
+
100
271
  export function getWebSocketUrl(path = '/ws') {
101
272
  if (typeof window === 'undefined') return normalizePath(path)
102
273
 
103
274
  const base = getBackendUrl() || window.location.origin
104
- const url = new URL(normalizePath(path), base)
105
- url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
275
+ return buildWebSocketUrl(base, path)
276
+ }
277
+
278
+ export async function getAuthenticatedWebSocketUrl(path = '/ws') {
279
+ if (typeof window === 'undefined') return normalizePath(path)
280
+
281
+ const base = getBackendUrl() || window.location.origin
282
+ const url = new URL(buildWebSocketUrl(base, path))
283
+
284
+ const invite = getBackendInvite()
285
+ if (invite && shouldAttachBackendInvite(url.toString())) {
286
+ url.searchParams.set('invite', invite)
287
+ }
288
+
289
+ const identity = getStoredIdentity()
290
+ if (identity?.danger) {
291
+ try {
292
+ const auth = await buildAuthHeaders(
293
+ identity,
294
+ 'GET',
295
+ normalizeAuthPath(path)
296
+ )
297
+ const [address, timestamp, signature] = String(
298
+ auth.Authorization || ''
299
+ ).split(',')
300
+ if (address && signature) {
301
+ url.searchParams.set('address', address)
302
+ url.searchParams.set('timestamp', timestamp)
303
+ url.searchParams.set('signature', signature)
304
+ }
305
+ } catch {
306
+ // Leave WebSocket unauthenticated when local identity data is invalid.
307
+ }
308
+ }
309
+
106
310
  return url.toString()
107
311
  }
108
312
 
109
313
  export async function checkBackendConnection() {
110
314
  const url = getBackendUrl()
315
+ const invite = shouldAttachBackendInvite(url) ? getBackendInvite() : ''
316
+ return checkBackendConnectionTarget({ url, invite })
317
+ }
318
+
319
+ async function probeHttp(cleanedUrl, invite, identity) {
111
320
  try {
112
- const res = await fetch(`${url}/api/node-id`, {
321
+ const headers = {}
322
+ if (invite) headers['x-mostbox-invite'] = invite
323
+ try {
324
+ Object.assign(
325
+ headers,
326
+ await buildAuthHeaders(identity, 'GET', '/api/remote/capabilities')
327
+ )
328
+ } catch {
329
+ // Backend detection should still work when old identity data is invalid.
330
+ }
331
+ const res = await fetch(`${cleanedUrl}/api/remote/capabilities`, {
113
332
  method: 'GET',
333
+ headers,
114
334
  signal: AbortSignal.timeout(3000),
115
335
  })
116
- return res.ok
336
+ if (!res.ok) return { ok: false, reason: 'http' }
337
+ return { ok: true }
117
338
  } catch {
118
- return false
339
+ return { ok: false, reason: 'http' }
340
+ }
341
+ }
342
+
343
+ async function probeWebSocket(cleanedUrl, invite, identity) {
344
+ if (typeof WebSocket === 'undefined') return { ok: true }
345
+ if (!identity?.danger) return { ok: true }
346
+
347
+ try {
348
+ const wsUrl = new URL(buildWebSocketUrl(cleanedUrl))
349
+
350
+ if (invite) {
351
+ wsUrl.searchParams.set('invite', invite)
352
+ }
353
+
354
+ if (identity?.danger) {
355
+ try {
356
+ const auth = await buildAuthHeaders(
357
+ identity,
358
+ 'GET',
359
+ normalizeAuthPath('/ws')
360
+ )
361
+ const [address, timestamp, signature] = String(
362
+ auth.Authorization || ''
363
+ ).split(',')
364
+ if (address && signature) {
365
+ wsUrl.searchParams.set('address', address)
366
+ wsUrl.searchParams.set('timestamp', timestamp)
367
+ wsUrl.searchParams.set('signature', signature)
368
+ }
369
+ } catch {
370
+ // Leave WebSocket unauthenticated when local identity data is invalid.
371
+ }
372
+ }
373
+
374
+ return await new Promise(resolve => {
375
+ const ws = new WebSocket(wsUrl.toString())
376
+ const timeout = setTimeout(() => {
377
+ ws.close()
378
+ resolve({ ok: false, reason: 'ws' })
379
+ }, 4000)
380
+
381
+ ws.onopen = () => {
382
+ clearTimeout(timeout)
383
+ ws.close()
384
+ resolve({ ok: true })
385
+ }
386
+
387
+ ws.onerror = () => {
388
+ clearTimeout(timeout)
389
+ resolve({ ok: false, reason: 'ws' })
390
+ }
391
+ })
392
+ } catch {
393
+ return { ok: false, reason: 'ws' }
119
394
  }
120
395
  }
121
396
 
397
+ export async function checkBackendConnectionTarget({ url, invite = '' }) {
398
+ const cleanedUrl = normalizeBackendUrl(url)
399
+ if (!cleanedUrl) return { ok: false, reason: 'http' }
400
+
401
+ const identity = getStoredIdentity()
402
+
403
+ const [httpResult, wsResult] = await Promise.all([
404
+ probeHttp(cleanedUrl, invite, identity),
405
+ probeWebSocket(cleanedUrl, invite, identity),
406
+ ])
407
+
408
+ if (!httpResult.ok) return httpResult
409
+ if (!wsResult.ok) return wsResult
410
+ return { ok: true }
411
+ }
412
+
122
413
  export async function detectSameOriginBackend() {
123
414
  if (isLocalFrontendOrigin()) return false
124
415
 
@@ -0,0 +1,63 @@
1
+ import { verifyMessage } from 'ethers'
2
+ import { mostSignMessage } from './mostWallet.js'
3
+
4
+ export const AUTH_MAX_AGE_MS = 5 * 60 * 1000
5
+
6
+ export function normalizeAddress(address) {
7
+ const value = String(address || '').trim()
8
+ return /^0x[a-fA-F0-9]{40}$/.test(value) ? value.toLowerCase() : ''
9
+ }
10
+
11
+ export function buildAuthMessage(timestamp, method, path) {
12
+ return `${timestamp}:${String(method || 'GET').toUpperCase()}:${normalizeAuthPath(path)}`
13
+ }
14
+
15
+ export function normalizeAuthPath(path) {
16
+ try {
17
+ return new URL(path, 'http://most.box').pathname
18
+ } catch {
19
+ return String(path || '').split('?')[0] || '/'
20
+ }
21
+ }
22
+
23
+ export async function buildAuthHeaders(identity, method, path) {
24
+ if (!identity?.danger) return {}
25
+ const timestamp = Date.now().toString()
26
+ const message = buildAuthMessage(timestamp, method, path)
27
+ const { address, signature } = await mostSignMessage(identity.danger, message)
28
+ return {
29
+ Authorization: `${address},${timestamp},${signature}`,
30
+ }
31
+ }
32
+
33
+ export function verifyAuthHeader(header, method, path, options = {}) {
34
+ const [addressRaw, timestampRaw, signature] = String(header || '').split(',')
35
+ const address = normalizeAddress(addressRaw)
36
+ const timestamp = Number(timestampRaw)
37
+ const now = options.now || Date.now()
38
+
39
+ if (!address || !Number.isFinite(timestamp) || !signature) {
40
+ return { ok: false, error: 'Missing or invalid authorization' }
41
+ }
42
+ if (Math.abs(now - timestamp) > (options.maxAgeMs || AUTH_MAX_AGE_MS)) {
43
+ return { ok: false, error: 'Authorization expired' }
44
+ }
45
+
46
+ try {
47
+ const message = buildAuthMessage(timestampRaw, method, path)
48
+ const recovered = normalizeAddress(verifyMessage(message, signature))
49
+ if (recovered !== address) {
50
+ return { ok: false, error: 'Authorization address mismatch' }
51
+ }
52
+ return { ok: true, address }
53
+ } catch {
54
+ return { ok: false, error: 'Invalid authorization signature' }
55
+ }
56
+ }
57
+
58
+ export function parseInviteList(value = '') {
59
+ return String(value || '')
60
+ .split(',')
61
+ .map(item => item.trim())
62
+ .filter(Boolean)
63
+ }
@@ -0,0 +1,30 @@
1
+ import dayjs from 'dayjs'
2
+
3
+ export function formatDate(time) {
4
+ if (!time) return ''
5
+ const date = dayjs(Number(time))
6
+ const hour = date.hour()
7
+ let timeOfDay
8
+
9
+ if (hour >= 0 && hour < 3) {
10
+ timeOfDay = '凌晨'
11
+ } else if (hour >= 3 && hour < 6) {
12
+ timeOfDay = '拂晓'
13
+ } else if (hour >= 6 && hour < 9) {
14
+ timeOfDay = '早晨'
15
+ } else if (hour >= 9 && hour < 12) {
16
+ timeOfDay = '上午'
17
+ } else if (hour >= 12 && hour < 15) {
18
+ timeOfDay = '下午'
19
+ } else if (hour >= 15 && hour < 18) {
20
+ timeOfDay = '傍晚'
21
+ } else if (hour >= 18 && hour < 21) {
22
+ timeOfDay = '晚上'
23
+ } else {
24
+ timeOfDay = '深夜'
25
+ }
26
+
27
+ return date.format(`YYYY年M月D日 ${timeOfDay}h:m`)
28
+ }
29
+
30
+ export const formatTime = formatDate
@@ -0,0 +1,89 @@
1
+ import { parseMostLink } from '../core/mostLink.js'
2
+
3
+ const DOWNLOAD_CHECK_MESSAGES = {
4
+ timeout:
5
+ '检测等待超时,暂时没有等到在线种子响应。请确认分享者或其他下载者仍在线做种,稍后再检测。',
6
+ offline: '无法连接本地节点,请确认 MostBox 后端正在运行后再检测。',
7
+ missingApi: '当前后端还没有检测接口,请重启 MostBox 后端后再试。',
8
+ validation:
9
+ '链接格式不正确,请粘贴完整的 most://<cid>?filename=... 分享链接。',
10
+ nameConflict: '下载目录已有同名文件,请先重命名或移走后再检测。',
11
+ noPeer:
12
+ '暂时没有发现在线种子。请确认分享者或其他下载者仍在线做种,稍后再检测。',
13
+ permission: '下载目录不可写,请检查目录权限后再检测。',
14
+ starting: '本地节点还没有启动完成,请稍等几秒后重新检测。',
15
+ server: '本地节点检测时出错,请稍后重试或查看节点日志。',
16
+ fallback: '检测未通过,请确认链接完整、发布者在线且本机网络正常。',
17
+ }
18
+
19
+ const LINK_VALIDATION_MESSAGES = {
20
+ 'Link must be a valid most:// URL':
21
+ '链接无法解析,请粘贴完整的 most://<cid>?filename=... 分享链接。',
22
+ 'Link must use most:// protocol': '链接协议不正确,应以 most:// 开头。',
23
+ 'Link path is not supported':
24
+ '链接里不应包含路径,请使用 most://<cid>?filename=... 格式。',
25
+ 'Invalid CID format': 'CID 无效,请确认 most:// 后面的内容没有缺失或被截断。',
26
+ 'Invalid CID format: CID v1 required':
27
+ 'CID 格式不符合 MostBox 要求,请确认分享链接完整。',
28
+ 'CID digest must be 32 bytes':
29
+ 'CID 格式不符合 MostBox 要求,请确认分享链接完整。',
30
+ 'filename is required':
31
+ '链接缺少 filename 参数,请复制完整分享链接后再检测。',
32
+ }
33
+
34
+ export function getDownloadCheckErrorMessageFromPayload(
35
+ data = {},
36
+ errorName = ''
37
+ ) {
38
+ if (errorName === 'TimeoutError') return DOWNLOAD_CHECK_MESSAGES.timeout
39
+ if (!data.status) return DOWNLOAD_CHECK_MESSAGES.offline
40
+ if (data.status === 404) return DOWNLOAD_CHECK_MESSAGES.missingApi
41
+
42
+ switch (data.code) {
43
+ case 'VALIDATION_ERROR':
44
+ return DOWNLOAD_CHECK_MESSAGES.validation
45
+ case 'CONFLICT':
46
+ return data.error
47
+ ? `${data.error},请先处理同名文件后再下载。`
48
+ : DOWNLOAD_CHECK_MESSAGES.nameConflict
49
+ case 'PEER_NOT_FOUND':
50
+ return DOWNLOAD_CHECK_MESSAGES.noPeer
51
+ case 'PERMISSION_ERROR':
52
+ return data.error
53
+ ? `下载目录不可写:${data.error}`
54
+ : DOWNLOAD_CHECK_MESSAGES.permission
55
+ case 'ENGINE_NOT_INITIALIZED':
56
+ return DOWNLOAD_CHECK_MESSAGES.starting
57
+ default:
58
+ break
59
+ }
60
+
61
+ if (data.status === 503) return DOWNLOAD_CHECK_MESSAGES.noPeer
62
+ if (data.status >= 500) return DOWNLOAD_CHECK_MESSAGES.server
63
+ return data.error
64
+ ? `检测未通过:${data.error}`
65
+ : DOWNLOAD_CHECK_MESSAGES.fallback
66
+ }
67
+
68
+ export function getDownloadLinkValidationMessage(link = '') {
69
+ const value = String(link || '').trim()
70
+ if (!value) return '请先粘贴 most:// 分享链接。'
71
+
72
+ const result = parseMostLink(value)
73
+ if (!result.error) {
74
+ return result.fileName?.trim()
75
+ ? null
76
+ : LINK_VALIDATION_MESSAGES['filename is required']
77
+ }
78
+
79
+ if (result.error.startsWith('Unsupported query parameter: ')) {
80
+ const unsupportedParam = result.error.slice(
81
+ 'Unsupported query parameter: '.length
82
+ )
83
+ return `链接包含暂不支持的参数 ${unsupportedParam},请只保留 filename。`
84
+ }
85
+
86
+ return (
87
+ LINK_VALIDATION_MESSAGES[result.error] || DOWNLOAD_CHECK_MESSAGES.validation
88
+ )
89
+ }
@@ -65,6 +65,13 @@ export class ConflictError extends AppError {
65
65
  }
66
66
  }
67
67
 
68
+ export class StorageCapacityError extends AppError {
69
+ constructor(message = 'Storage capacity exceeded') {
70
+ super(message, 'STORAGE_CAPACITY_ERROR')
71
+ this.name = 'StorageCapacityError'
72
+ }
73
+ }
74
+
68
75
  export class EngineNotInitializedError extends AppError {
69
76
  constructor(message = 'Engine not initialized. Call start() first.') {
70
77
  super(message, 'ENGINE_NOT_INITIALIZED')