underpost 3.0.1 → 3.0.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.
- package/CHANGELOG.md +352 -264
- package/CLI-HELP.md +3 -4
- package/README.md +2 -2
- package/bin/build.js +5 -0
- package/bin/deploy.js +18 -26
- package/bin/file.js +3 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/ipfs/configmap.yaml +7 -0
- package/package.json +6 -2
- package/src/api/file/file.controller.js +3 -13
- package/src/api/file/file.ref.json +0 -21
- package/src/cli/cluster.js +30 -38
- package/src/cli/index.js +6 -1
- package/src/cli/run.js +44 -2
- package/src/client/components/core/LoadingAnimation.js +2 -3
- package/src/client/components/core/Modal.js +3 -1
- package/src/client/components/core/PublicProfile.js +3 -3
- package/src/client/components/core/Router.js +34 -1
- package/src/client/components/core/Worker.js +1 -1
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +1 -1
- package/src/server/auth.js +18 -18
- package/src/server/ipfs-client.js +433 -0
- package/bin/ssl.js +0 -63
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
coreUI,
|
|
24
24
|
sanitizeRoute,
|
|
25
25
|
getQueryParams,
|
|
26
|
+
setRouterReady,
|
|
26
27
|
} from './Router.js';
|
|
27
28
|
import { NotificationManager } from './NotificationManager.js';
|
|
28
29
|
import { EventsUI } from './EventsUI.js';
|
|
@@ -322,7 +323,7 @@ const Modal = {
|
|
|
322
323
|
'body',
|
|
323
324
|
html`
|
|
324
325
|
<div
|
|
325
|
-
class="abs main-body-btn-container
|
|
326
|
+
class="abs main-body-btn-container"
|
|
326
327
|
style="top: ${options.heightTopBar + 50}px; z-index: 9; ${true ||
|
|
327
328
|
(options.mode && options.mode.match('right'))
|
|
328
329
|
? 'right'
|
|
@@ -1401,6 +1402,7 @@ const Modal = {
|
|
|
1401
1402
|
}
|
|
1402
1403
|
});
|
|
1403
1404
|
setTimeout(window.onresize);
|
|
1405
|
+
setRouterReady();
|
|
1404
1406
|
});
|
|
1405
1407
|
})();
|
|
1406
1408
|
break;
|
|
@@ -196,7 +196,7 @@ const PublicProfile = {
|
|
|
196
196
|
user: { _id: userId, username },
|
|
197
197
|
} = options;
|
|
198
198
|
const idModal = options.idModal || getId();
|
|
199
|
-
const profileId = `public-profile-${
|
|
199
|
+
const profileId = `public-profile-${username}`;
|
|
200
200
|
const waveAnimationId = `${profileId}-wave`;
|
|
201
201
|
const profileImageClass = `${profileId}-image`;
|
|
202
202
|
const profileContainerId = `${profileId}-container`;
|
|
@@ -673,7 +673,7 @@ const PublicProfile = {
|
|
|
673
673
|
"
|
|
674
674
|
>
|
|
675
675
|
<div
|
|
676
|
-
class="${profileId}-image-container"
|
|
676
|
+
class="${profileId}-image-container public-profile-image-container"
|
|
677
677
|
style="
|
|
678
678
|
position: relative;
|
|
679
679
|
width: 160px;
|
|
@@ -688,7 +688,7 @@ const PublicProfile = {
|
|
|
688
688
|
"
|
|
689
689
|
>
|
|
690
690
|
<img
|
|
691
|
-
class="${profileImageClass}"
|
|
691
|
+
class="${profileImageClass} public-profile-image"
|
|
692
692
|
style="
|
|
693
693
|
width: 100%;
|
|
694
694
|
height: 100%;
|
|
@@ -39,6 +39,36 @@ const coreUI = ['modal-menu', 'main-body', 'main-body-top', 'bottom-bar', 'board
|
|
|
39
39
|
*/
|
|
40
40
|
const closeModalRouteChangeEvents = {};
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Deferred promise that resolves once the full UI (including deferred slide-menu DOM
|
|
44
|
+
* setup in Modal) is ready. Any code that depends on the complete DOM — route handlers,
|
|
45
|
+
* session callbacks, panel updates, etc. — can simply `await RouterReady` instead of
|
|
46
|
+
* scattering individual null-checks across every microfrontend.
|
|
47
|
+
*
|
|
48
|
+
* Resolved by calling `setRouterReady()`, which should happen exactly once at the end
|
|
49
|
+
* of Modal's deferred slide-menu `setTimeout` block.
|
|
50
|
+
* @type {Promise<void>}
|
|
51
|
+
* @memberof PwaRouter
|
|
52
|
+
*/
|
|
53
|
+
let _routerReadyResolve;
|
|
54
|
+
const RouterReady = new Promise((resolve) => {
|
|
55
|
+
_routerReadyResolve = resolve;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Signals that the deferred UI setup is complete and the router (and any other
|
|
60
|
+
* awaiter of `RouterReady`) may safely access the full DOM.
|
|
61
|
+
* This must be called exactly once – typically at the end of Modal's deferred
|
|
62
|
+
* slide-menu `setTimeout` block.
|
|
63
|
+
* @memberof PwaRouter
|
|
64
|
+
*/
|
|
65
|
+
const setRouterReady = () => {
|
|
66
|
+
if (_routerReadyResolve) {
|
|
67
|
+
_routerReadyResolve();
|
|
68
|
+
_routerReadyResolve = undefined;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
42
72
|
/**
|
|
43
73
|
* Determines the base path for the application, often used for routing within a sub-directory.
|
|
44
74
|
* It checks the current URL's pathname and `window.Routes` to return the appropriate proxy path.
|
|
@@ -209,7 +239,8 @@ const Router = function (options = { Routes: () => {}, e: new PopStateEvent() })
|
|
|
209
239
|
* @param {object} RouterInstance - The router instance configuration, including the `Routes` function.
|
|
210
240
|
* @memberof PwaRouter
|
|
211
241
|
*/
|
|
212
|
-
const LoadRouter = function (RouterInstance) {
|
|
242
|
+
const LoadRouter = async function (RouterInstance) {
|
|
243
|
+
await RouterReady;
|
|
213
244
|
Router(RouterInstance);
|
|
214
245
|
window.onpopstate = (e) => {
|
|
215
246
|
Router({ ...RouterInstance, e });
|
|
@@ -466,4 +497,6 @@ export {
|
|
|
466
497
|
sanitizeRoute,
|
|
467
498
|
queryParamsChangeListeners,
|
|
468
499
|
listenQueryParamsChange,
|
|
500
|
+
setRouterReady,
|
|
501
|
+
RouterReady,
|
|
469
502
|
};
|
|
@@ -135,7 +135,7 @@ class PwaWorker {
|
|
|
135
135
|
const isInstall = await this.status();
|
|
136
136
|
if (!isInstall) await this.install();
|
|
137
137
|
await render();
|
|
138
|
-
LoadRouter(this.RouterInstance);
|
|
138
|
+
await LoadRouter(this.RouterInstance);
|
|
139
139
|
LoadingAnimation.removeSplashScreen();
|
|
140
140
|
if (this.devMode()) {
|
|
141
141
|
// const delayLiveReload = 1250;
|
package/src/index.js
CHANGED
|
@@ -99,7 +99,7 @@ class ExpressService {
|
|
|
99
99
|
|
|
100
100
|
if (origins && isDevProxyContext())
|
|
101
101
|
origins.push(devProxyHostFactory({ host, includeHttp: true, tls: isTlsDevProxy() }));
|
|
102
|
-
app.set('trust proxy',
|
|
102
|
+
app.set('trust proxy', 1);
|
|
103
103
|
|
|
104
104
|
app.use((req, res, next) => {
|
|
105
105
|
res.on('finish', () => {
|
package/src/server/auth.js
CHANGED
|
@@ -647,24 +647,24 @@ function applySecurity(app, opts = {}) {
|
|
|
647
647
|
}),
|
|
648
648
|
);
|
|
649
649
|
logger.info('Cors origin', origin);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
650
|
+
if (!process.env.DISABLE_API_RATE_LIMIT) {
|
|
651
|
+
// Rate limiting + slow down
|
|
652
|
+
const limiter = rateLimit({
|
|
653
|
+
windowMs: rate.windowMs,
|
|
654
|
+
max: rate.max,
|
|
655
|
+
standardHeaders: true,
|
|
656
|
+
legacyHeaders: false,
|
|
657
|
+
message: { error: 'Too many requests, please try again later.' },
|
|
658
|
+
});
|
|
659
|
+
app.use(limiter);
|
|
660
|
+
|
|
661
|
+
const speedLimiter = slowDown({
|
|
662
|
+
windowMs: slowdown.windowMs,
|
|
663
|
+
delayAfter: slowdown.delayAfter,
|
|
664
|
+
delayMs: () => slowdown.delayMs,
|
|
665
|
+
});
|
|
666
|
+
app.use(speedLimiter);
|
|
667
|
+
}
|
|
668
668
|
// Cookie parsing
|
|
669
669
|
app.use(cookieParser(process.env.JWT_SECRET));
|
|
670
670
|
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight IPFS HTTP client for communicating with a Kubo (go-ipfs) node
|
|
3
|
+
* and an IPFS Cluster daemon running side-by-side in the same StatefulSet.
|
|
4
|
+
*
|
|
5
|
+
* Kubo API (port 5001) – add / pin / cat content.
|
|
6
|
+
* Cluster API (port 9094) – replicate pins across the cluster.
|
|
7
|
+
*
|
|
8
|
+
* Uses native `FormData` + `Blob` (Node ≥ 18) for reliable multipart encoding.
|
|
9
|
+
*
|
|
10
|
+
* @module src/server/ipfs-client.js
|
|
11
|
+
* @namespace IpfsClient
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import stringify from 'fast-json-stable-stringify';
|
|
15
|
+
import { loggerFactory } from './logger.js';
|
|
16
|
+
|
|
17
|
+
const logger = loggerFactory(import.meta);
|
|
18
|
+
|
|
19
|
+
// ─────────────────────────────────────────────────────────
|
|
20
|
+
// URL helpers
|
|
21
|
+
// ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Base URL of the Kubo RPC API (port 5001).
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
const getIpfsApiUrl = () =>
|
|
28
|
+
process.env.IPFS_API_URL || `http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:5001`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Base URL of the IPFS Cluster REST API (port 9094).
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
const getClusterApiUrl = () =>
|
|
35
|
+
process.env.IPFS_CLUSTER_API_URL ||
|
|
36
|
+
`http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:9094`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Base URL of the IPFS HTTP Gateway (port 8080).
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
const getGatewayUrl = () =>
|
|
43
|
+
process.env.IPFS_GATEWAY_URL ||
|
|
44
|
+
`http://${process.env.NODE_ENV === 'development' ? 'localhost' : 'ipfs-cluster'}:8080`;
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────
|
|
47
|
+
// Core: add content
|
|
48
|
+
// ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} IpfsAddResult
|
|
52
|
+
* @property {string} cid – CID (Content Identifier) returned by the node.
|
|
53
|
+
* @property {number} size – Cumulative DAG size reported by the node.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add arbitrary bytes to the Kubo node AND pin them on the IPFS Cluster.
|
|
58
|
+
*
|
|
59
|
+
* 1. `POST /api/v0/add?pin=true` to Kubo (5001) – stores + locally pins.
|
|
60
|
+
* 2. `POST /pins/<CID>` to the Cluster REST API (9094) – replicates the pin
|
|
61
|
+
* across every peer so `GET /pins` on the cluster shows the content.
|
|
62
|
+
* 3. Copies into MFS so the Web UI "Files" section shows the file.
|
|
63
|
+
*
|
|
64
|
+
* @param {Buffer|string} content – raw bytes or a UTF-8 string to store.
|
|
65
|
+
* @param {string} [filename='data'] – logical filename for the upload.
|
|
66
|
+
* @param {string} [mfsPath] – optional full MFS path.
|
|
67
|
+
* When omitted defaults to `/pinned/<filename>`.
|
|
68
|
+
* @returns {Promise<IpfsAddResult|null>} `null` when the node is unreachable.
|
|
69
|
+
*/
|
|
70
|
+
const addToIpfs = async (content, filename = 'data', mfsPath) => {
|
|
71
|
+
const kuboUrl = getIpfsApiUrl();
|
|
72
|
+
const clusterUrl = getClusterApiUrl();
|
|
73
|
+
|
|
74
|
+
// Build multipart body using native FormData + Blob (Node ≥ 18).
|
|
75
|
+
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
|
|
76
|
+
const formData = new FormData();
|
|
77
|
+
formData.append('file', new Blob([buf]), filename);
|
|
78
|
+
|
|
79
|
+
// ── Step 1: add to Kubo ──────────────────────────────
|
|
80
|
+
let cid;
|
|
81
|
+
let size;
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${kuboUrl}/api/v0/add?pin=true&cid-version=1`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: formData,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const text = await res.text();
|
|
90
|
+
logger.error(`IPFS Kubo add failed (${res.status}): ${text}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const json = await res.json();
|
|
95
|
+
cid = json.Hash;
|
|
96
|
+
size = Number(json.Size);
|
|
97
|
+
logger.info(`IPFS Kubo add OK – CID: ${cid}, size: ${size}`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.warn(`IPFS Kubo node unreachable at ${kuboUrl}: ${err.message}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Step 2: pin to the Cluster ───────────────────────
|
|
104
|
+
try {
|
|
105
|
+
const clusterRes = await fetch(`${clusterUrl}/pins/${encodeURIComponent(cid)}`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!clusterRes.ok) {
|
|
110
|
+
const text = await clusterRes.text();
|
|
111
|
+
logger.warn(`IPFS Cluster pin failed (${clusterRes.status}): ${text}`);
|
|
112
|
+
} else {
|
|
113
|
+
logger.info(`IPFS Cluster pin OK – CID: ${cid}`);
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
logger.warn(`IPFS Cluster unreachable at ${clusterUrl}: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Step 3: copy into MFS so the Web UI "Files" section shows it ─
|
|
120
|
+
const destPath = mfsPath || `/pinned/${filename}`;
|
|
121
|
+
const destDir = destPath.substring(0, destPath.lastIndexOf('/')) || '/';
|
|
122
|
+
try {
|
|
123
|
+
// Ensure parent directory exists in MFS
|
|
124
|
+
await fetch(`${kuboUrl}/api/v0/files/mkdir?arg=${encodeURIComponent(destDir)}&parents=true`, { method: 'POST' });
|
|
125
|
+
|
|
126
|
+
// 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
|
+
|
|
131
|
+
// Copy the CID into MFS
|
|
132
|
+
const cpRes = await fetch(
|
|
133
|
+
`${kuboUrl}/api/v0/files/cp?arg=/ipfs/${encodeURIComponent(cid)}&arg=${encodeURIComponent(destPath)}`,
|
|
134
|
+
{ method: 'POST' },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!cpRes.ok) {
|
|
138
|
+
const text = await cpRes.text();
|
|
139
|
+
logger.warn(`IPFS MFS cp failed (${cpRes.status}): ${text}`);
|
|
140
|
+
} else {
|
|
141
|
+
logger.info(`IPFS MFS cp OK – ${destPath} → ${cid}`);
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
logger.warn(`IPFS MFS cp unreachable: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { cid, size };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// ─────────────────────────────────────────────────────────
|
|
151
|
+
// Convenience wrappers
|
|
152
|
+
// ─────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Add a JSON-serialisable object to IPFS.
|
|
156
|
+
*
|
|
157
|
+
* @param {any} obj – value to serialise.
|
|
158
|
+
* @param {string} [filename='data.json']
|
|
159
|
+
* @param {string} [mfsPath] – optional full MFS destination path.
|
|
160
|
+
* @returns {Promise<IpfsAddResult|null>}
|
|
161
|
+
*/
|
|
162
|
+
const addJsonToIpfs = async (obj, filename = 'data.json', mfsPath) => {
|
|
163
|
+
const payload = stringify(obj);
|
|
164
|
+
return addToIpfs(Buffer.from(payload, 'utf-8'), filename, mfsPath);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Add a binary buffer (e.g. a PNG image) to IPFS.
|
|
169
|
+
*
|
|
170
|
+
* @param {Buffer} buffer – raw image / file bytes.
|
|
171
|
+
* @param {string} filename – e.g. `"atlas.png"`.
|
|
172
|
+
* @param {string} [mfsPath] – optional full MFS destination path.
|
|
173
|
+
* @returns {Promise<IpfsAddResult|null>}
|
|
174
|
+
*/
|
|
175
|
+
const addBufferToIpfs = async (buffer, filename, mfsPath) => {
|
|
176
|
+
return addToIpfs(buffer, filename, mfsPath);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ─────────────────────────────────────────────────────────
|
|
180
|
+
// Pin management
|
|
181
|
+
// ─────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Explicitly pin an existing CID on both the Kubo node and the Cluster.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} cid
|
|
187
|
+
* @param {string} [type='recursive'] – `'recursive'` | `'direct'`
|
|
188
|
+
* @returns {Promise<boolean>} `true` when at least the Kubo pin succeeded.
|
|
189
|
+
*/
|
|
190
|
+
const pinCid = async (cid, type = 'recursive') => {
|
|
191
|
+
const kuboUrl = getIpfsApiUrl();
|
|
192
|
+
const clusterUrl = getClusterApiUrl();
|
|
193
|
+
let kuboOk = false;
|
|
194
|
+
|
|
195
|
+
// Kubo pin
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(`${kuboUrl}/api/v0/pin/add?arg=${encodeURIComponent(cid)}&type=${type}`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
const text = await res.text();
|
|
202
|
+
logger.error(`IPFS Kubo pin/add failed (${res.status}): ${text}`);
|
|
203
|
+
} else {
|
|
204
|
+
kuboOk = true;
|
|
205
|
+
logger.info(`IPFS Kubo pin OK – CID: ${cid} (${type})`);
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
logger.warn(`IPFS Kubo pin unreachable: ${err.message}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Cluster pin
|
|
212
|
+
try {
|
|
213
|
+
const clusterRes = await fetch(`${clusterUrl}/pins/${encodeURIComponent(cid)}`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
});
|
|
216
|
+
if (!clusterRes.ok) {
|
|
217
|
+
const text = await clusterRes.text();
|
|
218
|
+
logger.warn(`IPFS Cluster pin failed (${clusterRes.status}): ${text}`);
|
|
219
|
+
} else {
|
|
220
|
+
logger.info(`IPFS Cluster pin OK – CID: ${cid}`);
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
logger.warn(`IPFS Cluster pin unreachable: ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return kuboOk;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Unpin a CID from both the Kubo node and the Cluster.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} cid
|
|
233
|
+
* @returns {Promise<boolean>}
|
|
234
|
+
*/
|
|
235
|
+
const unpinCid = async (cid) => {
|
|
236
|
+
const kuboUrl = getIpfsApiUrl();
|
|
237
|
+
const clusterUrl = getClusterApiUrl();
|
|
238
|
+
let kuboOk = false;
|
|
239
|
+
|
|
240
|
+
// Cluster unpin
|
|
241
|
+
try {
|
|
242
|
+
const clusterRes = await fetch(`${clusterUrl}/pins/${encodeURIComponent(cid)}`, {
|
|
243
|
+
method: 'DELETE',
|
|
244
|
+
});
|
|
245
|
+
if (!clusterRes.ok) {
|
|
246
|
+
const text = await clusterRes.text();
|
|
247
|
+
logger.warn(`IPFS Cluster unpin failed (${clusterRes.status}): ${text}`);
|
|
248
|
+
} else {
|
|
249
|
+
logger.info(`IPFS Cluster unpin OK – CID: ${cid}`);
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
logger.warn(`IPFS Cluster unpin unreachable: ${err.message}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Kubo unpin
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetch(`${kuboUrl}/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
});
|
|
260
|
+
if (!res.ok) {
|
|
261
|
+
const text = await res.text();
|
|
262
|
+
// "not pinned or pinned indirectly" means the CID is already unpinned – treat as success
|
|
263
|
+
if (text.includes('not pinned')) {
|
|
264
|
+
kuboOk = true;
|
|
265
|
+
logger.info(`IPFS Kubo unpin – CID already not pinned: ${cid}`);
|
|
266
|
+
} else {
|
|
267
|
+
logger.warn(`IPFS Kubo pin/rm failed (${res.status}): ${text}`);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
kuboOk = true;
|
|
271
|
+
logger.info(`IPFS Kubo unpin OK – CID: ${cid}`);
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
logger.warn(`IPFS Kubo unpin unreachable: ${err.message}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return kuboOk;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ─────────────────────────────────────────────────────────
|
|
281
|
+
// Retrieval
|
|
282
|
+
// ─────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Retrieve raw bytes for a CID from the IPFS HTTP Gateway (port 8080).
|
|
286
|
+
*
|
|
287
|
+
* @param {string} cid
|
|
288
|
+
* @returns {Promise<Buffer|null>}
|
|
289
|
+
*/
|
|
290
|
+
const getFromIpfs = async (cid) => {
|
|
291
|
+
const url = getGatewayUrl();
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch(`${url}/ipfs/${encodeURIComponent(cid)}`);
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
logger.error(`IPFS gateway GET failed (${res.status}) for ${cid}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
299
|
+
return Buffer.from(arrayBuffer);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
logger.warn(`IPFS gateway unreachable at ${url}: ${err.message}`);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// ─────────────────────────────────────────────────────────
|
|
307
|
+
// Diagnostics
|
|
308
|
+
// ─────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* List all pins tracked by the IPFS Cluster (port 9094).
|
|
312
|
+
* Each line in the response is a JSON object with at least a `cid` field.
|
|
313
|
+
*
|
|
314
|
+
* @returns {Promise<Array<{ cid: string, name: string, peer_map: object }>>}
|
|
315
|
+
*/
|
|
316
|
+
const listClusterPins = async () => {
|
|
317
|
+
const clusterUrl = getClusterApiUrl();
|
|
318
|
+
try {
|
|
319
|
+
const res = await fetch(`${clusterUrl}/pins`);
|
|
320
|
+
if (res.status === 204) {
|
|
321
|
+
// 204 No Content → the cluster has no pins at all.
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
if (!res.ok) {
|
|
325
|
+
const text = await res.text();
|
|
326
|
+
logger.error(`IPFS Cluster list pins failed (${res.status}): ${text}`);
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const text = await res.text();
|
|
330
|
+
// The cluster streams one JSON object per line (NDJSON).
|
|
331
|
+
return text
|
|
332
|
+
.split('\n')
|
|
333
|
+
.filter((line) => line.trim())
|
|
334
|
+
.map((line) => {
|
|
335
|
+
try {
|
|
336
|
+
return JSON.parse(line);
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.filter(Boolean);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
logger.warn(`IPFS Cluster unreachable at ${clusterUrl}: ${err.message}`);
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* List pins tracked by the local Kubo node (port 5001).
|
|
350
|
+
*
|
|
351
|
+
* @param {string} [type='recursive'] – `'all'` | `'recursive'` | `'direct'` | `'indirect'`
|
|
352
|
+
* @returns {Promise<Object<string, { Type: string }>>} Map of CID → pin info.
|
|
353
|
+
*/
|
|
354
|
+
const listKuboPins = async (type = 'recursive') => {
|
|
355
|
+
const kuboUrl = getIpfsApiUrl();
|
|
356
|
+
try {
|
|
357
|
+
const res = await fetch(`${kuboUrl}/api/v0/pin/ls?type=${type}`, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
});
|
|
360
|
+
if (!res.ok) {
|
|
361
|
+
const text = await res.text();
|
|
362
|
+
logger.error(`IPFS Kubo pin/ls failed (${res.status}): ${text}`);
|
|
363
|
+
return {};
|
|
364
|
+
}
|
|
365
|
+
const json = await res.json();
|
|
366
|
+
return json.Keys || {};
|
|
367
|
+
} catch (err) {
|
|
368
|
+
logger.warn(`IPFS Kubo pin/ls unreachable: ${err.message}`);
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// ─────────────────────────────────────────────────────────
|
|
374
|
+
// MFS management
|
|
375
|
+
// ─────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Remove a file or directory from the Kubo MFS (Mutable File System).
|
|
379
|
+
* This cleans up entries visible in the IPFS Web UI "Files" section.
|
|
380
|
+
*
|
|
381
|
+
* @param {string} mfsPath – Full MFS path to remove, e.g. `/pinned/myfile.json`
|
|
382
|
+
* or `/object-layer/itemId`.
|
|
383
|
+
* @param {boolean} [recursive=true] – When `true`, removes directories recursively.
|
|
384
|
+
* @returns {Promise<boolean>} `true` when the removal succeeded or the path didn't exist.
|
|
385
|
+
*/
|
|
386
|
+
const removeMfsPath = async (mfsPath, recursive = true) => {
|
|
387
|
+
const kuboUrl = getIpfsApiUrl();
|
|
388
|
+
try {
|
|
389
|
+
// First check if the path exists via stat; if it doesn't we can return early.
|
|
390
|
+
const statRes = await fetch(`${kuboUrl}/api/v0/files/stat?arg=${encodeURIComponent(mfsPath)}`, { method: 'POST' });
|
|
391
|
+
if (!statRes.ok) {
|
|
392
|
+
// Path doesn't exist – nothing to remove.
|
|
393
|
+
logger.info(`IPFS MFS rm – path does not exist, skipping: ${mfsPath}`);
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const rmRes = await fetch(
|
|
398
|
+
`${kuboUrl}/api/v0/files/rm?arg=${encodeURIComponent(mfsPath)}&force=true${recursive ? '&recursive=true' : ''}`,
|
|
399
|
+
{ method: 'POST' },
|
|
400
|
+
);
|
|
401
|
+
if (!rmRes.ok) {
|
|
402
|
+
const text = await rmRes.text();
|
|
403
|
+
logger.warn(`IPFS MFS rm failed (${rmRes.status}): ${text}`);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
logger.info(`IPFS MFS rm OK – ${mfsPath}`);
|
|
407
|
+
return true;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
logger.warn(`IPFS MFS rm unreachable: ${err.message}`);
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// ─────────────────────────────────────────────────────────
|
|
415
|
+
// Export
|
|
416
|
+
// ─────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
const IpfsClient = {
|
|
419
|
+
getIpfsApiUrl,
|
|
420
|
+
getClusterApiUrl,
|
|
421
|
+
getGatewayUrl,
|
|
422
|
+
addToIpfs,
|
|
423
|
+
addJsonToIpfs,
|
|
424
|
+
addBufferToIpfs,
|
|
425
|
+
pinCid,
|
|
426
|
+
unpinCid,
|
|
427
|
+
getFromIpfs,
|
|
428
|
+
listClusterPins,
|
|
429
|
+
listKuboPins,
|
|
430
|
+
removeMfsPath,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
export { IpfsClient };
|
package/bin/ssl.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import read from 'read';
|
|
3
|
-
import dotenv from 'dotenv';
|
|
4
|
-
import { getRootDirectory, pbcopy, shellExec } from '../src/server/process.js';
|
|
5
|
-
import { loggerFactory } from '../src/server/logger.js';
|
|
6
|
-
import { Cmd, loadConf } from '../src/server/conf.js';
|
|
7
|
-
import { buildSSL } from '../src/server/ssl.js';
|
|
8
|
-
|
|
9
|
-
dotenv.config();
|
|
10
|
-
|
|
11
|
-
const logger = loggerFactory(import.meta);
|
|
12
|
-
|
|
13
|
-
await logger.setUpInfo();
|
|
14
|
-
|
|
15
|
-
const [exe, dir, deployId, hosts] = process.argv;
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
let cmd;
|
|
19
|
-
await loadConf(deployId);
|
|
20
|
-
shellExec(Cmd.conf(deployId));
|
|
21
|
-
const confServer = JSON.parse(fs.readFileSync(`./conf/conf.server.json`, 'utf8'));
|
|
22
|
-
for (const host of hosts.split(',')) {
|
|
23
|
-
if (host in confServer) {
|
|
24
|
-
const directory = confServer[host]['/']?.['directory'] ? confServer[host]['/']['directory'] : undefined;
|
|
25
|
-
cmd = `sudo certbot certonly --webroot --webroot-path ${
|
|
26
|
-
directory ? directory : `${getRootDirectory()}/public/${host}`
|
|
27
|
-
} --cert-name ${host} -d ${host}`;
|
|
28
|
-
// directory ? directory : `${getRootDirectory()}/public/${host}`
|
|
29
|
-
// directory ? directory : `${getRootDirectory()}/public/www.${host.split('.').slice(-2).join('.')}`
|
|
30
|
-
|
|
31
|
-
// You can get multi domain cert by specifying (extra) -d
|
|
32
|
-
// For example
|
|
33
|
-
// certbot -d example.com -d example.net -d www.example.org
|
|
34
|
-
|
|
35
|
-
// delete all file (no increment live folder)
|
|
36
|
-
// certbot delete --cert-name <domain>
|
|
37
|
-
|
|
38
|
-
logger.info(`Run the following command`, cmd);
|
|
39
|
-
try {
|
|
40
|
-
await pbcopy(cmd);
|
|
41
|
-
await read({ prompt: 'Command copy to clipboard, press enter to continue.\n' });
|
|
42
|
-
} catch (error) {
|
|
43
|
-
logger.error(error);
|
|
44
|
-
}
|
|
45
|
-
// Certificate
|
|
46
|
-
if (process.argv.includes('build')) await buildSSL(host);
|
|
47
|
-
logger.info('Certificate saved', host);
|
|
48
|
-
} else throw new Error(`host not found: ${host}`);
|
|
49
|
-
}
|
|
50
|
-
// check for renewal conf:
|
|
51
|
-
// /etc/letsencrypt/renewal
|
|
52
|
-
// /etc/letsencrypt/live
|
|
53
|
-
cmd = `sudo certbot renew --dry-run`;
|
|
54
|
-
try {
|
|
55
|
-
await pbcopy(cmd);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
logger.error(error);
|
|
58
|
-
}
|
|
59
|
-
logger.info(`run the following command for renewal. Command copy to clipboard`, cmd);
|
|
60
|
-
logger.info(`success install SLL`, hosts);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
logger.error(error, error.stack);
|
|
63
|
-
}
|