underpost 3.2.4 → 3.2.8

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 (141) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +268 -1
  3. package/CLI-HELP.md +26 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +3 -3
  6. package/bin/build.js +13 -3
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +5 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -3
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -3
  13. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  14. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  15. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  16. package/package.json +20 -11
  17. package/src/api/core/core.controller.js +10 -10
  18. package/src/api/core/core.service.js +10 -10
  19. package/src/api/default/default.controller.js +10 -10
  20. package/src/api/default/default.service.js +10 -10
  21. package/src/api/document/document.controller.js +12 -12
  22. package/src/api/document/document.model.js +10 -16
  23. package/src/api/file/file.controller.js +8 -8
  24. package/src/api/file/file.model.js +10 -10
  25. package/src/api/file/file.service.js +36 -36
  26. package/src/api/test/test.controller.js +8 -8
  27. package/src/api/test/test.service.js +8 -8
  28. package/src/api/user/guest.service.js +99 -0
  29. package/src/api/user/user.controller.js +6 -6
  30. package/src/api/user/user.model.js +8 -13
  31. package/src/api/user/user.service.js +3 -20
  32. package/src/cli/deploy.js +33 -30
  33. package/src/cli/fs.js +62 -5
  34. package/src/cli/image.js +43 -1
  35. package/src/cli/index.js +5 -1
  36. package/src/cli/release.js +58 -2
  37. package/src/cli/repository.js +35 -3
  38. package/src/cli/run.js +304 -38
  39. package/src/cli/ssh.js +1 -1
  40. package/src/cli/static.js +43 -115
  41. package/src/client/Default.index.js +21 -33
  42. package/src/client/components/core/404.js +4 -4
  43. package/src/client/components/core/500.js +4 -4
  44. package/src/client/components/core/Account.js +73 -60
  45. package/src/client/components/core/AgGrid.js +23 -33
  46. package/src/client/components/core/Alert.js +12 -13
  47. package/src/client/components/core/AppStore.js +1 -1
  48. package/src/client/components/core/Auth.js +20 -32
  49. package/src/client/components/core/Badge.js +7 -13
  50. package/src/client/components/core/BtnIcon.js +15 -17
  51. package/src/client/components/core/CalendarCore.js +42 -63
  52. package/src/client/components/core/Chat.js +13 -15
  53. package/src/client/components/core/ClientEvents.js +87 -0
  54. package/src/client/components/core/ColorPaletteElement.js +309 -0
  55. package/src/client/components/core/Content.js +17 -14
  56. package/src/client/components/core/Css.js +15 -71
  57. package/src/client/components/core/CssCore.js +12 -16
  58. package/src/client/components/core/D3Chart.js +4 -4
  59. package/src/client/components/core/Docs.js +60 -59
  60. package/src/client/components/core/DropDown.js +69 -91
  61. package/src/client/components/core/EventBus.js +92 -0
  62. package/src/client/components/core/EventsUI.js +14 -17
  63. package/src/client/components/core/FileExplorer.js +102 -234
  64. package/src/client/components/core/FullScreen.js +47 -75
  65. package/src/client/components/core/Input.js +24 -69
  66. package/src/client/components/core/Keyboard.js +25 -18
  67. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  68. package/src/client/components/core/LoadingAnimation.js +25 -31
  69. package/src/client/components/core/LogIn.js +41 -41
  70. package/src/client/components/core/LogOut.js +23 -14
  71. package/src/client/components/core/Modal.js +397 -176
  72. package/src/client/components/core/NotificationManager.js +14 -18
  73. package/src/client/components/core/Panel.js +54 -50
  74. package/src/client/components/core/PanelForm.js +25 -125
  75. package/src/client/components/core/Polyhedron.js +110 -214
  76. package/src/client/components/core/PublicProfile.js +39 -32
  77. package/src/client/components/core/Recover.js +52 -48
  78. package/src/client/components/core/Responsive.js +88 -32
  79. package/src/client/components/core/RichText.js +9 -18
  80. package/src/client/components/core/Router.js +24 -3
  81. package/src/client/components/core/SearchBox.js +37 -37
  82. package/src/client/components/core/SignUp.js +39 -30
  83. package/src/client/components/core/SocketIo.js +31 -2
  84. package/src/client/components/core/SocketIoHandler.js +6 -6
  85. package/src/client/components/core/ToggleSwitch.js +8 -20
  86. package/src/client/components/core/ToolTip.js +5 -17
  87. package/src/client/components/core/Translate.js +56 -59
  88. package/src/client/components/core/Validator.js +26 -16
  89. package/src/client/components/core/Wallet.js +15 -26
  90. package/src/client/components/core/Worker.js +140 -25
  91. package/src/client/components/core/windowGetDimensions.js +7 -7
  92. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  93. package/src/client/components/default/CssDefault.js +12 -12
  94. package/src/client/components/default/LogInDefault.js +6 -4
  95. package/src/client/components/default/LogOutDefault.js +6 -4
  96. package/src/client/components/default/RouterDefault.js +47 -0
  97. package/src/client/components/default/SettingsDefault.js +4 -4
  98. package/src/client/components/default/SignUpDefault.js +6 -4
  99. package/src/client/components/default/TranslateDefault.js +3 -3
  100. package/src/client/services/core/core.service.js +17 -49
  101. package/src/client/services/default/default.management.js +139 -242
  102. package/src/client/services/default/default.service.js +10 -16
  103. package/src/client/services/document/document.service.js +14 -19
  104. package/src/client/services/file/file.service.js +8 -13
  105. package/src/client/services/test/test.service.js +8 -13
  106. package/src/client/services/user/guest.service.js +79 -0
  107. package/src/client/services/user/user.management.js +5 -5
  108. package/src/client/services/user/user.service.js +14 -20
  109. package/src/client/ssr/body/404.js +3 -3
  110. package/src/client/ssr/body/500.js +3 -3
  111. package/src/client/ssr/body/CacheControl.js +5 -2
  112. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  113. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  114. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  115. package/src/client/ssr/offline/Maintenance.js +12 -11
  116. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  117. package/src/client/ssr/pages/Test.js +2 -2
  118. package/src/client/sw/core.sw.js +212 -0
  119. package/src/index.js +1 -1
  120. package/src/runtime/express/Dockerfile +4 -4
  121. package/src/runtime/lampp/Dockerfile +8 -7
  122. package/src/runtime/wp/Dockerfile +11 -17
  123. package/src/server/backup.js +1 -2
  124. package/src/server/client-build-docs.js +45 -46
  125. package/src/server/client-build.js +334 -60
  126. package/src/server/client-formatted.js +47 -16
  127. package/src/server/conf.js +29 -13
  128. package/src/server/cron.js +6 -8
  129. package/src/server/dns.js +2 -1
  130. package/src/server/ipfs-client.js +232 -91
  131. package/src/server/process.js +13 -27
  132. package/src/server/start.js +6 -3
  133. package/src/server/valkey.js +134 -235
  134. package/tsconfig.docs.json +15 -0
  135. package/typedoc.json +20 -0
  136. package/jsdoc.json +0 -52
  137. package/src/client/components/core/ColorPalette.js +0 -5267
  138. package/src/client/components/core/JoyStick.js +0 -80
  139. package/src/client/components/default/RoutesDefault.js +0 -49
  140. package/src/client/sw/default.sw.js +0 -127
  141. package/src/client/sw/template.sw.js +0 -84
@@ -10,23 +10,43 @@
10
10
  * @module src/server/ipfs-client.js
11
11
  * @namespace IpfsClient
12
12
  */
13
-
14
13
  import stringify from 'fast-json-stable-stringify';
15
14
  import { loggerFactory } from './logger.js';
16
-
17
15
  const logger = loggerFactory(import.meta);
18
-
16
+ const DEFAULT_IPFS_HTTP_TIMEOUT_MS = Number(process.env.IPFS_HTTP_TIMEOUT_MS || 10000);
17
+ const getRequestTimeoutMs = (kind = 'kubo') => {
18
+ if (kind === 'cluster') {
19
+ return Number(process.env.IPFS_CLUSTER_TIMEOUT_MS || DEFAULT_IPFS_HTTP_TIMEOUT_MS);
20
+ }
21
+ if (kind === 'gateway') {
22
+ return Number(process.env.IPFS_GATEWAY_TIMEOUT_MS || DEFAULT_IPFS_HTTP_TIMEOUT_MS);
23
+ }
24
+ return Number(process.env.IPFS_KUBO_TIMEOUT_MS || DEFAULT_IPFS_HTTP_TIMEOUT_MS);
25
+ };
26
+ const fetchWithTimeout = async (url, options = {}, { kind = 'kubo', label = url } = {}) => {
27
+ const controller = new AbortController();
28
+ const timeoutMs = getRequestTimeoutMs(kind);
29
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
30
+ try {
31
+ return await fetch(url, { ...options, signal: controller.signal });
32
+ } catch (err) {
33
+ if (err.name === 'AbortError') {
34
+ throw new Error(`${label} timed out after ${timeoutMs}ms`);
35
+ }
36
+ throw err;
37
+ } finally {
38
+ clearTimeout(timeoutId);
39
+ }
40
+ };
19
41
  // ─────────────────────────────────────────────────────────
20
42
  // URL helpers
21
43
  // ─────────────────────────────────────────────────────────
22
-
23
44
  /**
24
45
  * Base URL of the Kubo RPC API (port 5001).
25
46
  * @returns {string}
26
47
  */
27
48
  const getIpfsApiUrl = () =>
28
49
  process.env.IPFS_API_URL || `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:5001`;
29
-
30
50
  /**
31
51
  * Base URL of the IPFS Cluster REST API (port 9094).
32
52
  * @returns {string}
@@ -34,7 +54,6 @@ const getIpfsApiUrl = () =>
34
54
  const getClusterApiUrl = () =>
35
55
  process.env.IPFS_CLUSTER_API_URL ||
36
56
  `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:9094`;
37
-
38
57
  /**
39
58
  * Base URL of the IPFS HTTP Gateway (port 8080).
40
59
  * @returns {string}
@@ -42,17 +61,14 @@ const getClusterApiUrl = () =>
42
61
  const getGatewayUrl = () =>
43
62
  process.env.IPFS_GATEWAY_URL ||
44
63
  `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:8080`;
45
-
46
64
  // ─────────────────────────────────────────────────────────
47
65
  // Core: add content
48
66
  // ─────────────────────────────────────────────────────────
49
-
50
67
  /**
51
68
  * @typedef {Object} IpfsAddResult
52
69
  * @property {string} cid – CID (Content Identifier) returned by the node.
53
70
  * @property {number} size – Cumulative DAG size reported by the node.
54
71
  */
55
-
56
72
  /**
57
73
  * Add arbitrary bytes to the Kubo node AND pin them on the IPFS Cluster.
58
74
  *
@@ -70,27 +86,27 @@ const getGatewayUrl = () =>
70
86
  const addToIpfs = async (content, filename = 'data', mfsPath) => {
71
87
  const kuboUrl = getIpfsApiUrl();
72
88
  const clusterUrl = getClusterApiUrl();
73
-
74
89
  // Build multipart body using native FormData + Blob (Node ≥ 18).
75
90
  const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
76
91
  const formData = new FormData();
77
92
  formData.append('file', new Blob([buf]), filename);
78
-
79
93
  // ── Step 1: add to Kubo ──────────────────────────────
80
94
  let cid;
81
95
  let size;
82
96
  try {
83
- const res = await fetch(`${kuboUrl}/api/v0/add?pin=true&cid-version=1`, {
84
- method: 'POST',
85
- body: formData,
86
- });
87
-
97
+ const res = await fetchWithTimeout(
98
+ `${kuboUrl}/api/v0/add?pin=true&cid-version=1`,
99
+ {
100
+ method: 'POST',
101
+ body: formData,
102
+ },
103
+ { kind: 'kubo', label: `IPFS Kubo add ${filename}` },
104
+ );
88
105
  if (!res.ok) {
89
106
  const text = await res.text();
90
107
  logger.error(`IPFS Kubo add failed (${res.status}): ${text}`);
91
108
  return null;
92
109
  }
93
-
94
110
  const json = await res.json();
95
111
  cid = json.Hash;
96
112
  size = Number(json.Size);
@@ -99,13 +115,15 @@ const addToIpfs = async (content, filename = 'data', mfsPath) => {
99
115
  logger.warn(`IPFS Kubo node unreachable at ${kuboUrl}: ${err.message}`);
100
116
  return null;
101
117
  }
102
-
103
118
  // ── Step 2: pin to the Cluster ───────────────────────
104
119
  try {
105
- const clusterRes = await fetch(`${clusterUrl}/pins/${encodeURIComponent(cid)}`, {
106
- method: 'POST',
107
- });
108
-
120
+ const clusterRes = await fetchWithTimeout(
121
+ `${clusterUrl}/pins/${encodeURIComponent(cid)}`,
122
+ {
123
+ method: 'POST',
124
+ },
125
+ { kind: 'cluster', label: `IPFS Cluster pin ${cid}` },
126
+ );
109
127
  if (!clusterRes.ok) {
110
128
  const text = await clusterRes.text();
111
129
  logger.warn(`IPFS Cluster pin failed (${clusterRes.status}): ${text}`);
@@ -115,25 +133,30 @@ const addToIpfs = async (content, filename = 'data', mfsPath) => {
115
133
  } catch (err) {
116
134
  logger.warn(`IPFS Cluster unreachable at ${clusterUrl}: ${err.message}`);
117
135
  }
118
-
119
136
  // ── Step 3: copy into MFS so the Web UI "Files" section shows it ─
120
137
  const destPath = mfsPath || `/pinned/${filename}`;
121
138
  const destDir = destPath.substring(0, destPath.lastIndexOf('/')) || '/';
122
139
  try {
123
140
  // Ensure parent directory exists in MFS
124
- await fetch(`${kuboUrl}/api/v0/files/mkdir?arg=${encodeURIComponent(destDir)}&parents=true`, { method: 'POST' });
125
-
141
+ await fetchWithTimeout(
142
+ `${kuboUrl}/api/v0/files/mkdir?arg=${encodeURIComponent(destDir)}&parents=true`,
143
+ { method: 'POST' },
144
+ { kind: 'kubo', label: `IPFS MFS mkdir ${destDir}` },
145
+ );
126
146
  // Remove existing entry if present (cp fails on duplicates)
127
- await fetch(`${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(destPath)}&force=true`, {
128
- method: 'POST',
129
- });
130
-
147
+ await fetchWithTimeout(
148
+ `${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(destPath)}&force=true`,
149
+ {
150
+ method: 'POST',
151
+ },
152
+ { kind: 'kubo', label: `IPFS MFS rm ${destPath}` },
153
+ );
131
154
  // Copy the CID into MFS
132
- const cpRes = await fetch(
155
+ const cpRes = await fetchWithTimeout(
133
156
  `${kuboUrl}/api/v0/files/cp?arg=/ipfs/${encodeURIComponent(cid)}&arg=${encodeURIComponent(destPath)}`,
134
157
  { method: 'POST' },
158
+ { kind: 'kubo', label: `IPFS MFS cp ${destPath}` },
135
159
  );
136
-
137
160
  if (!cpRes.ok) {
138
161
  const text = await cpRes.text();
139
162
  logger.warn(`IPFS MFS cp failed (${cpRes.status}): ${text}`);
@@ -143,14 +166,11 @@ const addToIpfs = async (content, filename = 'data', mfsPath) => {
143
166
  } catch (err) {
144
167
  logger.warn(`IPFS MFS cp unreachable: ${err.message}`);
145
168
  }
146
-
147
169
  return { cid, size };
148
170
  };
149
-
150
171
  // ─────────────────────────────────────────────────────────
151
172
  // Convenience wrappers
152
173
  // ─────────────────────────────────────────────────────────
153
-
154
174
  /**
155
175
  * Add a JSON-serialisable object to IPFS.
156
176
  *
@@ -163,7 +183,52 @@ const addJsonToIpfs = async (obj, filename = 'data.json', mfsPath) => {
163
183
  const payload = stringify(obj);
164
184
  return addToIpfs(Buffer.from(payload, 'utf-8'), filename, mfsPath);
165
185
  };
166
-
186
+ /**
187
+ * Compute the CID that Kubo would assign to a payload without pinning or copying it into MFS.
188
+ * Useful when building canonical backup manifests from the actual bytes that will be restored later.
189
+ *
190
+ * @param {Buffer|string} content
191
+ * @param {string} [filename='data']
192
+ * @returns {Promise<IpfsAddResult|null>}
193
+ */
194
+ const hashContentForIpfs = async (content, filename = 'data') => {
195
+ const kuboUrl = getIpfsApiUrl();
196
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
197
+ const formData = new FormData();
198
+ formData.append('file', new Blob([buf]), filename);
199
+ try {
200
+ const res = await fetchWithTimeout(
201
+ `${kuboUrl}/api/v0/add?only-hash=true&pin=false&cid-version=1`,
202
+ {
203
+ method: 'POST',
204
+ body: formData,
205
+ },
206
+ { kind: 'kubo', label: `IPFS Kubo only-hash ${filename}` },
207
+ );
208
+ if (!res.ok) {
209
+ const text = await res.text();
210
+ logger.error(`IPFS Kubo only-hash failed (${res.status}): ${text}`);
211
+ return null;
212
+ }
213
+ const json = await res.json();
214
+ return { cid: json.Hash, size: Number(json.Size) };
215
+ } catch (err) {
216
+ logger.warn(`IPFS Kubo only-hash unreachable at ${kuboUrl}: ${err.message}`);
217
+ return null;
218
+ }
219
+ };
220
+ /**
221
+ * Compute the CID for a JSON-serialisable object using the same stable stringification
222
+ * that the regular addJsonToIpfs path uses.
223
+ *
224
+ * @param {any} obj
225
+ * @param {string} [filename='data.json']
226
+ * @returns {Promise<IpfsAddResult|null>}
227
+ */
228
+ const hashJsonForIpfs = async (obj, filename = 'data.json') => {
229
+ const payload = stringify(obj);
230
+ return hashContentForIpfs(Buffer.from(payload, 'utf-8'), filename);
231
+ };
167
232
  /**
168
233
  * Add a binary buffer (e.g. a PNG image) to IPFS.
169
234
  *
@@ -175,11 +240,19 @@ const addJsonToIpfs = async (obj, filename = 'data.json', mfsPath) => {
175
240
  const addBufferToIpfs = async (buffer, filename, mfsPath) => {
176
241
  return addToIpfs(buffer, filename, mfsPath);
177
242
  };
178
-
243
+ /**
244
+ * Compute the CID for a binary buffer without pinning it.
245
+ *
246
+ * @param {Buffer} buffer
247
+ * @param {string} filename
248
+ * @returns {Promise<IpfsAddResult|null>}
249
+ */
250
+ const hashBufferForIpfs = async (buffer, filename) => {
251
+ return hashContentForIpfs(buffer, filename);
252
+ };
179
253
  // ─────────────────────────────────────────────────────────
180
254
  // Pin management
181
255
  // ─────────────────────────────────────────────────────────
182
-
183
256
  /**
184
257
  * Explicitly pin an existing CID on both the Kubo node and the Cluster.
185
258
  *
@@ -191,12 +264,15 @@ const pinCid = async (cid, type = 'recursive') => {
191
264
  const kuboUrl = getIpfsApiUrl();
192
265
  const clusterUrl = getClusterApiUrl();
193
266
  let kuboOk = false;
194
-
195
267
  // Kubo pin
196
268
  try {
197
- const res = await fetch(`${kuboUrl}/api/v0/pin/add?arg=${encodeURIComponent(cid)}&type=${type}`, {
198
- method: 'POST',
199
- });
269
+ const res = await fetchWithTimeout(
270
+ `${kuboUrl}/api/v0/pin/add?arg=${encodeURIComponent(cid)}&type=${type}`,
271
+ {
272
+ method: 'POST',
273
+ },
274
+ { kind: 'kubo', label: `IPFS Kubo pin/add ${cid}` },
275
+ );
200
276
  if (!res.ok) {
201
277
  const text = await res.text();
202
278
  logger.error(`IPFS Kubo pin/add failed (${res.status}): ${text}`);
@@ -207,12 +283,15 @@ const pinCid = async (cid, type = 'recursive') => {
207
283
  } catch (err) {
208
284
  logger.warn(`IPFS Kubo pin unreachable: ${err.message}`);
209
285
  }
210
-
211
286
  // Cluster pin
212
287
  try {
213
- const clusterRes = await fetch(`${clusterUrl}/pins/${encodeURIComponent(cid)}`, {
214
- method: 'POST',
215
- });
288
+ const clusterRes = await fetchWithTimeout(
289
+ `${clusterUrl}/pins/${encodeURIComponent(cid)}`,
290
+ {
291
+ method: 'POST',
292
+ },
293
+ { kind: 'cluster', label: `IPFS Cluster pin ${cid}` },
294
+ );
216
295
  if (!clusterRes.ok) {
217
296
  const text = await clusterRes.text();
218
297
  logger.warn(`IPFS Cluster pin failed (${clusterRes.status}): ${text}`);
@@ -222,10 +301,8 @@ const pinCid = async (cid, type = 'recursive') => {
222
301
  } catch (err) {
223
302
  logger.warn(`IPFS Cluster pin unreachable: ${err.message}`);
224
303
  }
225
-
226
304
  return kuboOk;
227
305
  };
228
-
229
306
  /**
230
307
  * Unpin a CID from both the Kubo node and the Cluster.
231
308
  *
@@ -236,12 +313,15 @@ const unpinCid = async (cid) => {
236
313
  const kuboUrl = getIpfsApiUrl();
237
314
  const clusterUrl = getClusterApiUrl();
238
315
  let kuboOk = false;
239
-
240
316
  // Cluster unpin
241
317
  try {
242
- const clusterRes = await fetch(`${clusterUrl}/pins/${encodeURIComponent(cid)}`, {
243
- method: 'DELETE',
244
- });
318
+ const clusterRes = await fetchWithTimeout(
319
+ `${clusterUrl}/pins/${encodeURIComponent(cid)}`,
320
+ {
321
+ method: 'DELETE',
322
+ },
323
+ { kind: 'cluster', label: `IPFS Cluster unpin ${cid}` },
324
+ );
245
325
  if (!clusterRes.ok) {
246
326
  const text = await clusterRes.text();
247
327
  if (clusterRes.status === 404) {
@@ -255,12 +335,15 @@ const unpinCid = async (cid) => {
255
335
  } catch (err) {
256
336
  logger.warn(`IPFS Cluster unpin unreachable: ${err.message}`);
257
337
  }
258
-
259
338
  // Kubo unpin
260
339
  try {
261
- const res = await fetch(`${kuboUrl}/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`, {
262
- method: 'POST',
263
- });
340
+ const res = await fetchWithTimeout(
341
+ `${kuboUrl}/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`,
342
+ {
343
+ method: 'POST',
344
+ },
345
+ { kind: 'kubo', label: `IPFS Kubo pin/rm ${cid}` },
346
+ );
264
347
  if (!res.ok) {
265
348
  const text = await res.text();
266
349
  // "not pinned or pinned indirectly" means the CID is already unpinned – treat as success
@@ -277,14 +360,11 @@ const unpinCid = async (cid) => {
277
360
  } catch (err) {
278
361
  logger.warn(`IPFS Kubo unpin unreachable: ${err.message}`);
279
362
  }
280
-
281
363
  return kuboOk;
282
364
  };
283
-
284
365
  // ─────────────────────────────────────────────────────────
285
366
  // Retrieval
286
367
  // ─────────────────────────────────────────────────────────
287
-
288
368
  /**
289
369
  * Retrieve raw bytes for a CID from the IPFS HTTP Gateway (port 8080).
290
370
  *
@@ -294,7 +374,14 @@ const unpinCid = async (cid) => {
294
374
  const getFromIpfs = async (cid) => {
295
375
  const url = getGatewayUrl();
296
376
  try {
297
- const res = await fetch(`${url}/ipfs/${encodeURIComponent(cid)}`);
377
+ const res = await fetchWithTimeout(
378
+ `${url}/ipfs/${encodeURIComponent(cid)}`,
379
+ {},
380
+ {
381
+ kind: 'gateway',
382
+ label: `IPFS gateway GET ${cid}`,
383
+ },
384
+ );
298
385
  if (!res.ok) {
299
386
  logger.error(`IPFS gateway GET failed (${res.status}) for ${cid}`);
300
387
  return null;
@@ -306,11 +393,9 @@ const getFromIpfs = async (cid) => {
306
393
  return null;
307
394
  }
308
395
  };
309
-
310
396
  // ─────────────────────────────────────────────────────────
311
397
  // Diagnostics
312
398
  // ─────────────────────────────────────────────────────────
313
-
314
399
  /**
315
400
  * List all pins tracked by the IPFS Cluster (port 9094).
316
401
  * Each line in the response is a JSON object with at least a `cid` field.
@@ -320,7 +405,14 @@ const getFromIpfs = async (cid) => {
320
405
  const listClusterPins = async () => {
321
406
  const clusterUrl = getClusterApiUrl();
322
407
  try {
323
- const res = await fetch(`${clusterUrl}/pins`);
408
+ const res = await fetchWithTimeout(
409
+ `${clusterUrl}/pins`,
410
+ {},
411
+ {
412
+ kind: 'cluster',
413
+ label: 'IPFS Cluster list pins',
414
+ },
415
+ );
324
416
  if (res.status === 204) {
325
417
  // 204 No Content → the cluster has no pins at all.
326
418
  return [];
@@ -348,7 +440,6 @@ const listClusterPins = async () => {
348
440
  return [];
349
441
  }
350
442
  };
351
-
352
443
  /**
353
444
  * List pins tracked by the local Kubo node (port 5001).
354
445
  *
@@ -358,9 +449,13 @@ const listClusterPins = async () => {
358
449
  const listKuboPins = async (type = 'recursive') => {
359
450
  const kuboUrl = getIpfsApiUrl();
360
451
  try {
361
- const res = await fetch(`${kuboUrl}/api/v0/pin/ls?type=${type}`, {
362
- method: 'POST',
363
- });
452
+ const res = await fetchWithTimeout(
453
+ `${kuboUrl}/api/v0/pin/ls?type=${type}`,
454
+ {
455
+ method: 'POST',
456
+ },
457
+ { kind: 'kubo', label: `IPFS Kubo pin/ls type=${type}` },
458
+ );
364
459
  if (!res.ok) {
365
460
  const text = await res.text();
366
461
  logger.error(`IPFS Kubo pin/ls failed (${res.status}): ${text}`);
@@ -373,11 +468,9 @@ const listKuboPins = async (type = 'recursive') => {
373
468
  return {};
374
469
  }
375
470
  };
376
-
377
471
  // ─────────────────────────────────────────────────────────
378
472
  // MFS management
379
473
  // ─────────────────────────────────────────────────────────
380
-
381
474
  /**
382
475
  * Remove a file or directory from the Kubo MFS (Mutable File System).
383
476
  * This cleans up entries visible in the IPFS Web UI "Files" section.
@@ -391,16 +484,20 @@ const removeMfsPath = async (mfsPath, recursive = true) => {
391
484
  const kuboUrl = getIpfsApiUrl();
392
485
  try {
393
486
  // First check if the path exists via stat; if it doesn't we can return early.
394
- const statRes = await fetch(`${kuboUrl}/api/v0/files/stat?arg=${encodeURIComponent(mfsPath)}`, { method: 'POST' });
487
+ const statRes = await fetchWithTimeout(
488
+ `${kuboUrl}/api/v0/files/stat?arg=${encodeURIComponent(mfsPath)}`,
489
+ { method: 'POST' },
490
+ { kind: 'kubo', label: `IPFS MFS stat ${mfsPath}` },
491
+ );
395
492
  if (!statRes.ok) {
396
493
  // Path doesn't exist – nothing to remove.
397
494
  logger.info(`IPFS MFS rm – path does not exist, skipping: ${mfsPath}`);
398
495
  return true;
399
496
  }
400
-
401
- const rmRes = await fetch(
497
+ const rmRes = await fetchWithTimeout(
402
498
  `${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(mfsPath)}&force=true${recursive ? '&recursive=true' : ''}`,
403
499
  { method: 'POST' },
500
+ { kind: 'kubo', label: `IPFS MFS rm ${mfsPath}` },
404
501
  );
405
502
  if (!rmRes.ok) {
406
503
  const text = await rmRes.text();
@@ -414,24 +511,65 @@ const removeMfsPath = async (mfsPath, recursive = true) => {
414
511
  return false;
415
512
  }
416
513
  };
417
-
514
+ /**
515
+ * Restore a CID into the Kubo MFS at a specific path (e.g. when re-importing a backup).
516
+ * Creates the parent directory if needed, removes any existing entry, then copies the CID.
517
+ *
518
+ * @param {string} cid – IPFS CID to copy into MFS.
519
+ * @param {string} mfsPath – Full destination MFS path, e.g. `/object-layer/sword/sword_data.json`.
520
+ * @returns {Promise<boolean>} `true` when the MFS entry was created successfully.
521
+ */
522
+ const restoreMfsPath = async (cid, mfsPath) => {
523
+ const kuboUrl = getIpfsApiUrl();
524
+ const destDir = mfsPath.substring(0, mfsPath.lastIndexOf('/')) || '/';
525
+ try {
526
+ await fetchWithTimeout(
527
+ `${kuboUrl}/api/v0/files/mkdir?arg=${encodeURIComponent(destDir)}&parents=true`,
528
+ { method: 'POST' },
529
+ { kind: 'kubo', label: `IPFS MFS mkdir ${destDir}` },
530
+ );
531
+ await fetchWithTimeout(
532
+ `${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(mfsPath)}&force=true`,
533
+ { method: 'POST' },
534
+ { kind: 'kubo', label: `IPFS MFS rm ${mfsPath}` },
535
+ );
536
+ const cpRes = await fetchWithTimeout(
537
+ `${kuboUrl}/api/v0/files/cp?arg=/ipfs/${encodeURIComponent(cid)}&arg=${encodeURIComponent(mfsPath)}`,
538
+ { method: 'POST' },
539
+ { kind: 'kubo', label: `IPFS MFS restore ${mfsPath}` },
540
+ );
541
+ if (!cpRes.ok) {
542
+ const text = await cpRes.text();
543
+ logger.warn(`IPFS MFS restore failed (${cpRes.status}): ${text} – ${mfsPath}`);
544
+ return false;
545
+ }
546
+ logger.info(`IPFS MFS restore OK – ${mfsPath} → ${cid}`);
547
+ return true;
548
+ } catch (err) {
549
+ logger.warn(`IPFS MFS restore unreachable: ${err.message}`);
550
+ return false;
551
+ }
552
+ };
418
553
  // ─────────────────────────────────────────────────────────
419
554
  // Export
420
555
  // ─────────────────────────────────────────────────────────
421
-
422
- const IpfsClient = {
423
- getIpfsApiUrl,
424
- getClusterApiUrl,
425
- getGatewayUrl,
426
- addToIpfs,
427
- addJsonToIpfs,
428
- addBufferToIpfs,
429
- pinCid,
430
- unpinCid,
431
- getFromIpfs,
432
- listClusterPins,
433
- listKuboPins,
434
- removeMfsPath,
556
+ class IpfsClient {
557
+ static getIpfsApiUrl = getIpfsApiUrl;
558
+ static getClusterApiUrl = getClusterApiUrl;
559
+ static getGatewayUrl = getGatewayUrl;
560
+ static addToIpfs = addToIpfs;
561
+ static addJsonToIpfs = addJsonToIpfs;
562
+ static addBufferToIpfs = addBufferToIpfs;
563
+ static hashContentForIpfs = hashContentForIpfs;
564
+ static hashJsonForIpfs = hashJsonForIpfs;
565
+ static hashBufferForIpfs = hashBufferForIpfs;
566
+ static pinCid = pinCid;
567
+ static unpinCid = unpinCid;
568
+ static getFromIpfs = getFromIpfs;
569
+ static listClusterPins = listClusterPins;
570
+ static listKuboPins = listKuboPins;
571
+ static removeMfsPath = removeMfsPath;
572
+ static restoreMfsPath = restoreMfsPath;
435
573
  /**
436
574
  * Check whether a single CID is currently pinned on the local Kubo node.
437
575
  * Uses the pin/ls?arg=<cid> endpoint which returns only that one pin
@@ -440,17 +578,20 @@ const IpfsClient = {
440
578
  * @param {string} cid - IPFS Content Identifier to check.
441
579
  * @returns {Promise<boolean>} true when the CID is pinned.
442
580
  */
443
- isCidPinned: async (cid) => {
581
+ static isCidPinned = async (cid) => {
444
582
  const kuboUrl = getIpfsApiUrl();
445
583
  try {
446
- const res = await fetch(`${kuboUrl}/api/v0/pin/ls?arg=${encodeURIComponent(cid)}&type=all`, { method: 'POST' });
584
+ const res = await fetchWithTimeout(
585
+ `${kuboUrl}/api/v0/pin/ls?arg=${encodeURIComponent(cid)}&type=all`,
586
+ { method: 'POST' },
587
+ { kind: 'kubo', label: `IPFS Kubo pin/ls ${cid}` },
588
+ );
447
589
  if (!res.ok) return false;
448
590
  const json = await res.json();
449
591
  return !!(json.Keys && json.Keys[cid]);
450
592
  } catch {
451
593
  return false;
452
594
  }
453
- },
454
- };
455
-
595
+ };
596
+ }
456
597
  export { IpfsClient };