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
@@ -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,119 @@ 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 buildWebSocketUrl(base, wsPath = '/ws') {
87
+ const url = new URL(base)
88
+ const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
89
+ const basePath = url.pathname.replace(/\/+$/, '')
90
+ return `${wsProtocol}//${url.host}${basePath}${normalizePath(wsPath)}`
91
+ }
92
+
28
93
  function createApiInstance() {
29
- const url = getBackendUrl()
30
- return ky.create({
31
- prefix: url,
94
+ const client = ky.create({
95
+ hooks: {
96
+ beforeRequest: [
97
+ async ({ request }) => {
98
+ const headers = new Headers(request.headers || {})
99
+ const invite = getBackendInvite()
100
+ if (invite && shouldAttachBackendInvite(request.url)) {
101
+ headers.set('x-mostbox-invite', invite)
102
+ }
103
+
104
+ const identity = getStoredIdentity()
105
+ if (identity?.danger) {
106
+ try {
107
+ const authHeaders = await buildAuthHeaders(
108
+ identity,
109
+ request.method,
110
+ normalizeAuthPath(request.url)
111
+ )
112
+ for (const [key, value] of Object.entries(authHeaders)) {
113
+ headers.set(key, value)
114
+ }
115
+ } catch {
116
+ // Ignore invalid legacy identity data for public/backend probes.
117
+ }
118
+ }
119
+ return new Request(request, { headers })
120
+ },
121
+ ],
122
+ },
123
+ })
124
+
125
+ return new Proxy(client, {
126
+ get(target, prop, receiver) {
127
+ const value = Reflect.get(target, prop, receiver)
128
+ if (!['get', 'post', 'put', 'patch', 'delete', 'head'].includes(prop)) {
129
+ return value
130
+ }
131
+ return (input, options) => {
132
+ const nextInput = typeof input === 'string' ? getApiUrl(input) : input
133
+ return value.call(target, nextInput, options)
134
+ }
135
+ },
32
136
  })
33
137
  }
34
138
 
@@ -71,7 +175,7 @@ export async function getApiErrorMessage(err, fallback = '请求失败') {
71
175
  err && typeof err === 'object' && 'name' in err ? String(err.name) : ''
72
176
  if (errorName === 'TimeoutError') return '请求超时,请稍后重试'
73
177
 
74
- if (!data.status && err instanceof Error && err.message) {
178
+ if (err instanceof Error && err.message) {
75
179
  return err.message
76
180
  }
77
181
 
@@ -79,7 +183,7 @@ export async function getApiErrorMessage(err, fallback = '请求失败') {
79
183
  }
80
184
 
81
185
  export function setBackendUrl(url) {
82
- const cleaned = (url || '').trim().replace(/\/+$/, '')
186
+ const cleaned = normalizeBackendUrl(url)
83
187
  if (cleaned) {
84
188
  localStorage.setItem(STORAGE_KEY, cleaned)
85
189
  } else {
@@ -88,37 +192,206 @@ export function setBackendUrl(url) {
88
192
  api = createApiInstance()
89
193
  }
90
194
 
195
+ export function setBackendInvite(invite) {
196
+ const cleaned = (invite || '').trim()
197
+ if (cleaned) {
198
+ localStorage.setItem(INVITE_STORAGE_KEY, cleaned)
199
+ } else {
200
+ localStorage.removeItem(INVITE_STORAGE_KEY)
201
+ }
202
+ api = createApiInstance()
203
+ }
204
+
205
+ export function configureBackend({ url, invite }) {
206
+ setBackendUrl(url)
207
+ setBackendInvite(invite)
208
+ }
209
+
210
+ export function clearBackendConnection() {
211
+ setBackendUrl('')
212
+ setBackendInvite('')
213
+ }
214
+
91
215
  export function getBackendUrlExport() {
92
216
  return getBackendUrl()
93
217
  }
94
218
 
219
+ export function getRemoteBackendUrlExport() {
220
+ return getRemoteBackendUrl()
221
+ }
222
+
223
+ export function getBackendInviteExport() {
224
+ return getBackendInvite()
225
+ }
226
+
95
227
  export function getApiUrl(path) {
96
228
  const url = getBackendUrl()
97
229
  return `${url}${normalizePath(path)}`
98
230
  }
99
231
 
232
+ export async function getApiRequestHeaders(method = 'GET', path = '/') {
233
+ /** @type {Record<string, string>} */
234
+ const headers = {}
235
+ const invite = getBackendInvite()
236
+ if (invite && shouldAttachBackendInvite()) {
237
+ headers['x-mostbox-invite'] = invite
238
+ }
239
+ try {
240
+ Object.assign(
241
+ headers,
242
+ await buildAuthHeaders(
243
+ getStoredIdentity(),
244
+ method,
245
+ normalizeAuthPath(path)
246
+ )
247
+ )
248
+ } catch {
249
+ // Callers that require auth will receive the server's 401 response.
250
+ }
251
+ return headers
252
+ }
253
+
100
254
  export function getWebSocketUrl(path = '/ws') {
101
255
  if (typeof window === 'undefined') return normalizePath(path)
102
256
 
103
257
  const base = getBackendUrl() || window.location.origin
104
- const url = new URL(normalizePath(path), base)
105
- url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
258
+ return buildWebSocketUrl(base, path)
259
+ }
260
+
261
+ export async function getAuthenticatedWebSocketUrl(path = '/ws') {
262
+ if (typeof window === 'undefined') return normalizePath(path)
263
+
264
+ const base = getBackendUrl() || window.location.origin
265
+ const url = new URL(buildWebSocketUrl(base, path))
266
+
267
+ const invite = getBackendInvite()
268
+ if (invite && shouldAttachBackendInvite(url.toString())) {
269
+ url.searchParams.set('invite', invite)
270
+ }
271
+
272
+ const identity = getStoredIdentity()
273
+ if (identity?.danger) {
274
+ try {
275
+ const auth = await buildAuthHeaders(
276
+ identity,
277
+ 'GET',
278
+ normalizeAuthPath(path)
279
+ )
280
+ const [address, timestamp, signature] = String(
281
+ auth.Authorization || ''
282
+ ).split(',')
283
+ if (address && signature) {
284
+ url.searchParams.set('address', address)
285
+ url.searchParams.set('timestamp', timestamp)
286
+ url.searchParams.set('signature', signature)
287
+ }
288
+ } catch {
289
+ // Leave WebSocket unauthenticated when local identity data is invalid.
290
+ }
291
+ }
292
+
106
293
  return url.toString()
107
294
  }
108
295
 
109
296
  export async function checkBackendConnection() {
110
297
  const url = getBackendUrl()
298
+ const invite = shouldAttachBackendInvite(url) ? getBackendInvite() : ''
299
+ return checkBackendConnectionTarget({ url, invite })
300
+ }
301
+
302
+ async function probeHttp(cleanedUrl, invite, identity) {
111
303
  try {
112
- const res = await fetch(`${url}/api/node-id`, {
304
+ const headers = {}
305
+ if (invite) headers['x-mostbox-invite'] = invite
306
+ try {
307
+ Object.assign(
308
+ headers,
309
+ await buildAuthHeaders(identity, 'GET', '/api/remote/capabilities')
310
+ )
311
+ } catch {
312
+ // Backend detection should still work when old identity data is invalid.
313
+ }
314
+ const res = await fetch(`${cleanedUrl}/api/remote/capabilities`, {
113
315
  method: 'GET',
316
+ headers,
114
317
  signal: AbortSignal.timeout(3000),
115
318
  })
116
- return res.ok
319
+ if (!res.ok) return { ok: false, reason: 'http' }
320
+ return { ok: true }
117
321
  } catch {
118
- return false
322
+ return { ok: false, reason: 'http' }
323
+ }
324
+ }
325
+
326
+ async function probeWebSocket(cleanedUrl, invite, identity) {
327
+ if (typeof WebSocket === 'undefined') return { ok: true }
328
+
329
+ try {
330
+ const wsUrl = new URL(buildWebSocketUrl(cleanedUrl))
331
+
332
+ if (invite) {
333
+ wsUrl.searchParams.set('invite', invite)
334
+ }
335
+
336
+ if (identity?.danger) {
337
+ try {
338
+ const auth = await buildAuthHeaders(
339
+ identity,
340
+ 'GET',
341
+ normalizeAuthPath('/ws')
342
+ )
343
+ const [address, timestamp, signature] = String(
344
+ auth.Authorization || ''
345
+ ).split(',')
346
+ if (address && signature) {
347
+ wsUrl.searchParams.set('address', address)
348
+ wsUrl.searchParams.set('timestamp', timestamp)
349
+ wsUrl.searchParams.set('signature', signature)
350
+ }
351
+ } catch {
352
+ // Leave WebSocket unauthenticated when local identity data is invalid.
353
+ }
354
+ }
355
+
356
+ return await new Promise(resolve => {
357
+ const ws = new WebSocket(wsUrl.toString())
358
+ const timeout = setTimeout(() => {
359
+ ws.close()
360
+ resolve({ ok: false, reason: 'ws' })
361
+ }, 4000)
362
+
363
+ ws.onopen = () => {
364
+ clearTimeout(timeout)
365
+ ws.close()
366
+ resolve({ ok: true })
367
+ }
368
+
369
+ ws.onerror = () => {
370
+ clearTimeout(timeout)
371
+ resolve({ ok: false, reason: 'ws' })
372
+ }
373
+ })
374
+ } catch {
375
+ return { ok: false, reason: 'ws' }
119
376
  }
120
377
  }
121
378
 
379
+ export async function checkBackendConnectionTarget({ url, invite = '' }) {
380
+ const cleanedUrl = normalizeBackendUrl(url)
381
+ if (!cleanedUrl) return { ok: false, reason: 'http' }
382
+
383
+ const identity = getStoredIdentity()
384
+
385
+ const [httpResult, wsResult] = await Promise.all([
386
+ probeHttp(cleanedUrl, invite, identity),
387
+ probeWebSocket(cleanedUrl, invite, identity),
388
+ ])
389
+
390
+ if (!httpResult.ok) return httpResult
391
+ if (!wsResult.ok) return wsResult
392
+ return { ok: true }
393
+ }
394
+
122
395
  export async function detectSameOriginBackend() {
123
396
  if (isLocalFrontendOrigin()) return false
124
397
 
@@ -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')