underpost 3.0.1 → 3.0.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,61 @@
1
1
  # Changelog
2
2
 
3
- ## 2026-02-23
3
+ ## 2026-03-02
4
+
5
+ ### engine-cyberia
6
+
7
+ - Add test in engine-cyberia cd workflow ([642d7e36](https://github.com/underpostnet/engine/commit/642d7e36155b6db5dbd36e19767dd95f146ceaf5))
8
+ - Add build dd-cyberia shape generator exclusive module files ([188f563a](https://github.com/underpostnet/engine/commit/188f563a61d7f6bf36bef93cdd18d4e1304f9747))
9
+ - Fix ObjectLayerEngineViewer return to list button click ([aeaead6f](https://github.com/underpostnet/engine/commit/aeaead6f5c67a03449c63c584976e9a73ccc953d))
10
+ - Improve static generations assets in object layer generation CLI ([06694d92](https://github.com/underpostnet/engine/commit/06694d92ea1ad849e745f561b8ec9a48bfa66056))
11
+ - Implements deterministic object layer generation CLI ([f70c9841](https://github.com/underpostnet/engine/commit/f70c9841ef2efc9187c87427cc465505487766db))
12
+ - Implement shape generator module ([5741a38b](https://github.com/underpostnet/engine/commit/5741a38bcfb8c1c4e0ef5053a2a6a73ff50a3879))
13
+ - Fix remove of ag grid table delete object layer delete logic ([e98953cd](https://github.com/underpostnet/engine/commit/e98953cd29767ca44c2362997f0af40cd538371b))
14
+ - Centralize Object Layer Logic and add js docs ([ff8eefed](https://github.com/underpostnet/engine/commit/ff8eefed08349a1e3390379f760c0d9eb20aecca))
15
+ - ObjectLayer Engine Viewer Enhancements ([0ee052e5](https://github.com/underpostnet/engine/commit/0ee052e5231f7b55576595a817742970c90cd056))
16
+ - Add metada json editor of object layers ([abe7448f](https://github.com/underpostnet/engine/commit/abe7448f5ed7429ba1f5c5d01ed94c5c70323638))
17
+ - Remove helia pyntype logic ([2b443d1c](https://github.com/underpostnet/engine/commit/2b443d1c0ed2261e27d5be54903c9a37cff29dd5))
18
+ - Object Layer Deletion Cleanup IPFS ([a2dcdf23](https://github.com/underpostnet/engine/commit/a2dcdf238c32d5b5237f0650232aca0c0823f044))
19
+ - Add Public GET Access for File Object Atlas ([826317fe](https://github.com/underpostnet/engine/commit/826317fe21dfd0b77196ef343b31461c45b5eb72))
20
+ - Allow Cross-Origin on GET methods file, object-layer, and atlas-sprite-sheet api. ([6801839c](https://github.com/underpostnet/engine/commit/6801839cc461dbec6ca205b035ea844415779e85))
21
+ - Add DISABLE_API_RATE_LIMIT env option ([ae72885c](https://github.com/underpostnet/engine/commit/ae72885c1178846067db52b62455d804dbe4eeba))
22
+
23
+ ### client-core
24
+
25
+ - Fix main-body-btn-container hide logic ([221f8bfc](https://github.com/underpostnet/engine/commit/221f8bfc262048e1ca226f66f0dfab9891db3fd5))
26
+
27
+ ### runtime-express
28
+
29
+ - Fix express rate limit trust proxy ([ed19e729](https://github.com/underpostnet/engine/commit/ed19e729eafb59d46504fb1ebe89e4bd91c05d7e))
30
+
31
+ ### cli-cluster
32
+
33
+ - Remove unused full flag ([13df39f5](https://github.com/underpostnet/engine/commit/13df39f508d65b61378ccfca4f7bfc427dcf5fa5))
34
+
35
+ ### ipfs
36
+
37
+ - Add ipfs client stable json stringify in addJsonToIpfs method ([c2aaf56a](https://github.com/underpostnet/engine/commit/c2aaf56a4bfc4f06147818ec5681567e27967f41))
38
+ - Fix config map IPFS Cluster daemon bind ([7e6df963](https://github.com/underpostnet/engine/commit/7e6df963ba6da1fdc96ac5b6ab844a789901f61b))
39
+ - server ipfs client and object layer atlas sprite sheet ipfs integration ([781e35c4](https://github.com/underpostnet/engine/commit/781e35c4903380df9e7dce7cf5d9275387a46029))
40
+ - Implement ipfs api user-pin and client component service ([1b12e8df](https://github.com/underpostnet/engine/commit/1b12e8df6af21e1dd2edc156e176072f25c9a433))
41
+
42
+ ### cli-run
43
+
44
+ - Implements expose-ipfs runner ([765772b8](https://github.com/underpostnet/engine/commit/765772b8fb1e7b397560464d1dc6dea0b70a9b7f))
45
+
46
+ ### engine-core
47
+
48
+ - Clean up legacy logic and json file model ref ([b4c62a2c](https://github.com/underpostnet/engine/commit/b4c62a2cfe4fea0212be644ce333464a81056f6f))
49
+
50
+ ### bin-build
51
+
52
+ - Add missing packagejson overrides on dd-cyberia build repository workflow ([7ece9ed5](https://github.com/underpostnet/engine/commit/7ece9ed5500e83a1baedc4d78fd889bca6ecac3c))
53
+
54
+ ## New release v:3.0.1 (2026-02-22)
55
+
56
+ ### engine-core
57
+
58
+ - Remove ENABLE_FILE_LOGS to default dev adn test env ([727486dc](https://github.com/underpostnet/engine/commit/727486dc4030921c9d1f6a7035eb1a240569fa74))
4
59
 
5
60
  ### gitlab
6
61
 
package/CLI-HELP.md CHANGED
@@ -1,4 +1,4 @@
1
- ## underpost ci/cd cli v3.0.1
1
+ ## underpost ci/cd cli v3.0.2
2
2
 
3
3
  ### Usage: `underpost [options] [command]`
4
4
  ```
@@ -379,8 +379,6 @@ Options:
379
379
  --dedicated-gpu Initializes the cluster with dedicated
380
380
  GPU base resources and environment
381
381
  settings.
382
- --full Initializes the cluster with all
383
- available statefulsets and services.
384
382
  --ns-use <ns-name> Switches the current Kubernetes context
385
383
  to the specified namespace (creates if
386
384
  it doesn't exist).
@@ -835,7 +833,7 @@ Options:
835
833
  Runs specified scripts using various runners.
836
834
 
837
835
  Arguments:
838
- runner-id The runner ID to run. Options: dev-cluster,metadata,svc-ls,svc-rm,ssh-deploy-info,dev-hosts-expose,dev-hosts-restore,cluster-build,template-deploy,template-deploy-image,clean,pull,release-deploy,ssh-deploy,ide,crypto-policy,sync,stop,ssh-deploy-stop,ssh-deploy-db-rollback,ssh-deploy-db,ssh-deploy-db-status,tz,get-proxy,instance-promote,instance,ls-deployments,host-update,dd-container,ip-info,db-client,git-conf,promote,metrics,cluster,deploy,disk-clean,disk-devices,disk-usage,dev,service,etc-hosts,sh,log,ps,ptls,release-cmt,deploy-test,sync-replica,tf-vae-test,spark-template,rmi,kill,secret,underpost-config,gpu-env,tf-gpu-test,deploy-job.
836
+ runner-id The runner ID to run. Options: dev-cluster,ipfs-expose,metadata,svc-ls,svc-rm,ssh-deploy-info,dev-hosts-expose,dev-hosts-restore,cluster-build,template-deploy,template-deploy-image,clean,pull,release-deploy,ssh-deploy,ide,crypto-policy,sync,stop,ssh-deploy-stop,ssh-deploy-db-rollback,ssh-deploy-db,ssh-deploy-db-status,tz,get-proxy,instance-promote,instance,ls-deployments,host-update,dd-container,ip-info,db-client,git-conf,promote,metrics,cluster,deploy,disk-clean,disk-devices,disk-usage,dev,service,etc-hosts,sh,log,ps,ptls,release-cmt,deploy-test,sync-replica,tf-vae-test,spark-template,rmi,kill,secret,underpost-config,gpu-env,tf-gpu-test,deploy-job.
839
837
  path The input value, identifier, or path for the operation.
840
838
 
841
839
  Options:
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  <div align="center">
18
18
 
19
- [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/3.0.1)](https://socket.dev/npm/package/underpost/overview/3.0.1) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
19
+ [![Node.js CI](https://github.com/underpostnet/engine/actions/workflows/docker-image.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/docker-image.yml) [![Test](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml/badge.svg?branch=master)](https://github.com/underpostnet/engine/actions/workflows/coverall.ci.yml) [![Downloads](https://img.shields.io/npm/dm/underpost.svg)](https://www.npmjs.com/package/underpost) [![Socket Badge](https://socket.dev/api/badge/npm/package/underpost/3.0.2)](https://socket.dev/npm/package/underpost/overview/3.0.2) [![Coverage Status](https://coveralls.io/repos/github/underpostnet/engine/badge.svg?branch=master)](https://coveralls.io/github/underpostnet/engine?branch=master) [![Version](https://img.shields.io/npm/v/underpost.svg)](https://www.npmjs.org/package/underpost) [![License](https://img.shields.io/npm/l/underpost.svg)](https://www.npmjs.com/package/underpost)
20
20
 
21
21
  </div>
22
22
 
@@ -60,7 +60,7 @@ npm run dev
60
60
 
61
61
  <a target="_top" href="https://www.nexodev.org/docs?cid=src">See Docs here.</a>
62
62
 
63
- ## underpost ci/cd cli v3.0.1
63
+ ## underpost ci/cd cli v3.0.2
64
64
 
65
65
  ### Usage: `underpost [options] [command]`
66
66
  ```
package/bin/build.js CHANGED
@@ -172,6 +172,7 @@ const { DefaultConf } = await import(`../conf.${confName}.js`);
172
172
  ...packageJson.dependencies,
173
173
  ...CyberiaDependencies,
174
174
  };
175
+ packageJson.overrides = originPackageJson.overrides;
175
176
  fs.writeFileSync(`${basePath}/bin/index.js`, fs.readFileSync(`./bin/cyberia.js`, 'utf8'), 'utf8');
176
177
  fs.copyFileSync(`./src/api/object-layer/README.md`, `${basePath}/README.md`);
177
178
  fs.copySync(`./hardhat`, `${basePath}/hardhat`);
@@ -180,6 +181,9 @@ const { DefaultConf } = await import(`../conf.${confName}.js`);
180
181
  '/src/client/ssr/pages/CyberiaServerMetrics.js',
181
182
  '/src/server/object-layer.js',
182
183
  '/src/server/atlas-sprite-sheet-generator.js',
184
+ '/src/server/shape-generator.js',
185
+ '/src/server/semantic-layer-generator.js',
186
+ '/test/shape-generator.test.js',
183
187
  ])
184
188
  fs.copySync(`.${path}`, `${basePath}${path}`);
185
189
 
@@ -216,4 +220,6 @@ const { DefaultConf } = await import(`../conf.${confName}.js`);
216
220
  if (fs.existsSync(`./src/ws/${confName.split('-')[1]}`)) {
217
221
  fs.copySync(`./src/ws/${confName.split('-')[1]}`, `${basePath}/src/ws/${confName.split('-')[1]}`);
218
222
  }
223
+ shellExec(`cd ${basePath} && npm install --ignore-scripts`);
224
+ shellExec(`cd ${basePath} && npm run fix`);
219
225
  }
package/bin/deploy.js CHANGED
@@ -374,7 +374,9 @@ try {
374
374
  shellExec(`node bin run kill 4002`);
375
375
  shellExec(`node bin run kill 4003`);
376
376
  shellExec(`npm run update-template`);
377
- shellExec(`cd ../pwa-microservices-template && npm install`);
377
+ shellExec(
378
+ `cd ../pwa-microservices-template && npm install && echo "\nENABLE_FILE_LOGS=true" >> .env.development`,
379
+ );
378
380
  shellExec(`cd ../pwa-microservices-template && npm run build && timeout 5s npm run dev`, {
379
381
  async: true,
380
382
  });
@@ -509,33 +511,23 @@ ${shellExec(`git log | grep Author: | sort -u`, { stdout: true }).split(`\n`).jo
509
511
  // https://besu.hyperledger.org/
510
512
  // https://github.com/hyperledger/besu/archive/refs/tags/24.9.1.tar.gz
511
513
 
512
- switch (process.platform) {
513
- case 'linux':
514
- {
515
- shellCd(`..`);
516
-
517
- // Download the Linux binary
518
- shellExec(`wget https://github.com/hyperledger/besu/releases/download/24.9.1/besu-24.9.1.tar.gz`);
514
+ shellCd(`..`);
519
515
 
520
- // Unzip the file:
521
- shellExec(`tar -xvzf besu-24.9.1.tar.gz`);
516
+ // Download the Linux binary
517
+ shellExec(`wget https://github.com/hyperledger/besu/releases/download/24.9.1/besu-24.9.1.tar.gz`);
522
518
 
523
- shellCd(`besu-24.9.1`);
519
+ // Unzip the file:
520
+ shellExec(`tar -xvzf besu-24.9.1.tar.gz`);
524
521
 
525
- shellExec(`bin/besu --help`);
522
+ shellCd(`besu-24.9.1`);
526
523
 
527
- // Set env path
528
- // export PATH=$PATH:/home/dd/besu-24.9.1/bin
524
+ shellExec(`bin/besu --help`);
529
525
 
530
- // Open src
531
- // shellExec(`sudo code /home/dd/besu-24.9.1 --user-data-dir="/root/.vscode-root" --no-sandbox`);
532
- }
533
-
534
- break;
526
+ // Set env path
527
+ // export PATH=$PATH:/home/dd/besu-24.9.1/bin
535
528
 
536
- default:
537
- break;
538
- }
529
+ // Open src
530
+ // shellExec(`sudo code /home/dd/besu-24.9.1 --user-data-dir="/root/.vscode-root" --no-sandbox`);
539
531
 
540
532
  break;
541
533
  }
@@ -977,10 +969,10 @@ nvidia/gpu-operator \
977
969
  `${key}`.toUpperCase().match('MAC')
978
970
  ? 'changethis'
979
971
  : isNaN(parseFloat(privateEnv[key]))
980
- ? `${privateEnv[key]}`.match(`@`)
981
- ? 'admin@default.net'
982
- : 'changethis'
983
- : privateEnv[key];
972
+ ? `${privateEnv[key]}`.match(`@`)
973
+ ? 'admin@default.net'
974
+ : 'changethis'
975
+ : privateEnv[key];
984
976
  }
985
977
  return env;
986
978
  };
package/bin/file.js CHANGED
@@ -97,6 +97,9 @@ try {
97
97
  './manifests/deployment/dd-template-development',
98
98
  './src/server/object-layer.js',
99
99
  './src/server/atlas-sprite-sheet-generator.js',
100
+ './src/server/shape-generator.js',
101
+ './src/server/semantic-layer-generator.js',
102
+ './test/shape-generator.test.js',
100
103
  'bin/cyberia.js',
101
104
  ]) {
102
105
  if (fs.existsSync(deletePath)) fs.removeSync('../pwa-microservices-template/' + deletePath);
@@ -23,7 +23,7 @@ spec:
23
23
  spec:
24
24
  containers:
25
25
  - name: dd-cron-backup
26
- image: underpost/underpost-engine:v3.0.1
26
+ image: underpost/underpost-engine:v3.0.2
27
27
  command:
28
28
  - /bin/sh
29
29
  - -c
@@ -23,7 +23,7 @@ spec:
23
23
  spec:
24
24
  containers:
25
25
  - name: dd-cron-dns
26
- image: underpost/underpost-engine:v3.0.1
26
+ image: underpost/underpost-engine:v3.0.2
27
27
  command:
28
28
  - /bin/sh
29
29
  - -c
@@ -17,7 +17,7 @@ spec:
17
17
  spec:
18
18
  containers:
19
19
  - name: dd-default-development-blue
20
- image: localhost/rockylinux9-underpost:v3.0.1
20
+ image: localhost/rockylinux9-underpost:v3.0.2
21
21
  # resources:
22
22
  # requests:
23
23
  # memory: "124Ki"
@@ -100,7 +100,7 @@ spec:
100
100
  spec:
101
101
  containers:
102
102
  - name: dd-default-development-green
103
- image: localhost/rockylinux9-underpost:v3.0.1
103
+ image: localhost/rockylinux9-underpost:v3.0.2
104
104
  # resources:
105
105
  # requests:
106
106
  # memory: "124Ki"
@@ -18,7 +18,7 @@ spec:
18
18
  spec:
19
19
  containers:
20
20
  - name: dd-test-development-blue
21
- image: localhost/rockylinux9-underpost:v3.0.1
21
+ image: localhost/rockylinux9-underpost:v3.0.2
22
22
 
23
23
  command:
24
24
  - /bin/sh
@@ -103,7 +103,7 @@ spec:
103
103
  spec:
104
104
  containers:
105
105
  - name: dd-test-development-green
106
- image: localhost/rockylinux9-underpost:v3.0.1
106
+ image: localhost/rockylinux9-underpost:v3.0.2
107
107
 
108
108
  command:
109
109
  - /bin/sh
@@ -16,6 +16,13 @@ data:
16
16
  ipfs-cluster-service init
17
17
  fi
18
18
 
19
+ # Bind cluster APIs to 0.0.0.0 so they are reachable from other pods.
20
+ # By default ipfs-cluster listens on 127.0.0.1 for REST (9094),
21
+ # Proxy (9095) and Pinning Service (9097).
22
+ sed -i 's|/ip4/127\.0\.0\.1/tcp/9094|/ip4/0.0.0.0/tcp/9094|g' /data/ipfs-cluster/service.json
23
+ sed -i 's|/ip4/127\.0\.0\.1/tcp/9095|/ip4/0.0.0.0/tcp/9095|g' /data/ipfs-cluster/service.json
24
+ sed -i 's|/ip4/127\.0\.0\.1/tcp/9097|/ip4/0.0.0.0/tcp/9097|g' /data/ipfs-cluster/service.json
25
+
19
26
  PEER_HOSTNAME=$(cat /proc/sys/kernel/hostname)
20
27
 
21
28
  if echo "${PEER_HOSTNAME}" | grep -q "^ipfs-cluster-0"; then
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "main": "src/index.js",
4
4
  "name": "underpost",
5
- "version": "3.0.1",
5
+ "version": "3.0.2",
6
6
  "description": "pwa api rest template",
7
7
  "scripts": {
8
8
  "start": "env-cmd -f .env.production node --max-old-space-size=8192 src/server",
@@ -19,22 +19,12 @@ const FileController = {
19
19
  },
20
20
  get: async (req, res, options) => {
21
21
  try {
22
+ res.setHeader('Access-Control-Allow-Origin', '*');
23
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
22
24
  const result = await FileService.get(req, res, options);
23
25
  if (result instanceof Buffer) {
24
- if (
25
- process.env.NODE_ENV === 'development' ||
26
- req.hostname === options.host ||
27
- (options.origins && options.origins.find((o) => o.match(req.hostname)))
28
- ) {
29
- res.set('Cross-Origin-Resource-Policy', 'cross-origin');
30
- return res.status(200).end(result);
31
- }
32
- return res.status(403).json({
33
- status: 'error',
34
- message: 'Forbidden',
35
- });
26
+ return res.status(200).end(result);
36
27
  }
37
-
38
28
  return res.status(200).json({
39
29
  status: 'success',
40
30
  data: result,
@@ -5,27 +5,6 @@
5
5
  "logo": true
6
6
  }
7
7
  },
8
- {
9
- "api": "cyberia-biome",
10
- "model": {
11
- "fileId": true,
12
- "topLevelColorFileId": true
13
- }
14
- },
15
- {
16
- "api": "cyberia-tile",
17
- "model": {
18
- "fileId": true
19
- }
20
- },
21
- {
22
- "api": "cyberia-world",
23
- "model": {
24
- "adjacentFace": {
25
- "fileId": true
26
- }
27
- }
28
- },
29
8
  {
30
9
  "api": "document",
31
10
  "model": {
@@ -37,7 +37,6 @@ class UnderpostCluster {
37
37
  * @param {boolean} [options.postgresql=false] - Deploy PostgreSQL.
38
38
  * @param {boolean} [options.valkey=false] - Deploy Valkey.
39
39
  * @param {boolean} [options.ipfs=false] - Deploy ipfs-cluster statefulset.
40
- * @param {boolean} [options.full=false] - Deploy a full set of common components.
41
40
  * @param {boolean} [options.info=false] - Display extensive Kubernetes cluster information.
42
41
  * @param {boolean} [options.certManager=false] - Deploy Cert-Manager for certificate management.
43
42
  * @param {boolean} [options.listPods=false] - List Kubernetes pods.
@@ -75,7 +74,6 @@ class UnderpostCluster {
75
74
  postgresql: false,
76
75
  valkey: false,
77
76
  ipfs: false,
78
- full: false,
79
77
  info: false,
80
78
  certManager: false,
81
79
  listPods: false,
@@ -102,26 +100,22 @@ class UnderpostCluster {
102
100
  replicas: '',
103
101
  },
104
102
  ) {
105
- // Handles initial host setup (installing docker, podman, kind, kubeadm, helm)
106
- if (options.initHost === true) return Underpost.cluster.initHost();
103
+ if (options.initHost) return Underpost.cluster.initHost();
107
104
 
108
- // Handles initial host setup (installing docker, podman, kind, kubeadm, helm)
109
- if (options.uninstallHost === true) return Underpost.cluster.uninstallHost();
105
+ if (options.uninstallHost) return Underpost.cluster.uninstallHost();
110
106
 
111
- // Applies general host configuration (SELinux, containerd, sysctl)
112
- if (options.config === true) return Underpost.cluster.config();
107
+ if (options.config) return Underpost.cluster.config();
113
108
 
114
- // Sets up kubectl configuration for the current user
115
- if (options.chown === true) return Underpost.cluster.chown();
109
+ if (options.chown) return Underpost.cluster.chown();
116
110
 
117
111
  const npmRoot = getNpmRootPath();
118
- const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
112
+ const underpostRoot = options.dev ? '.' : `${npmRoot}/underpost`;
119
113
 
120
- if (options.listPods === true) return console.table(Underpost.deploy.get(podName ?? undefined));
114
+ if (options.listPods) return console.table(Underpost.deploy.get(podName ?? undefined));
121
115
  // Set default namespace if not specified
122
116
  if (!options.namespace) options.namespace = 'default';
123
117
 
124
- if (options.nsUse && typeof options.nsUse === 'string') {
118
+ if (options.nsUse) {
125
119
  // Verify if namespace exists, create if not
126
120
  const namespaceExists = shellExec(`kubectl get namespace ${options.nsUse} --ignore-not-found -o name`, {
127
121
  stdout: true,
@@ -142,8 +136,8 @@ class UnderpostCluster {
142
136
  }
143
137
 
144
138
  // Reset Kubernetes cluster components (Kind/Kubeadm/K3s) and container runtimes
145
- if (options.reset === true) {
146
- const clusterType = options.k3s === true ? 'k3s' : options.kubeadm === true ? 'kubeadm' : 'kind';
139
+ if (options.reset) {
140
+ const clusterType = options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind';
147
141
  return await Underpost.cluster.safeReset({
148
142
  underpostRoot,
149
143
  removeVolumeHostPaths: options.removeVolumeHostPaths,
@@ -160,7 +154,7 @@ class UnderpostCluster {
160
154
  // --- Kubeadm/Kind/K3s Cluster Initialization ---
161
155
  if (!alreadyKubeadmCluster && !alreadyKindCluster && !alreadyK3sCluster) {
162
156
  Underpost.cluster.config();
163
- if (options.k3s === true) {
157
+ if (options.k3s) {
164
158
  logger.info('Initializing K3s control plane...');
165
159
  // Install K3s
166
160
  logger.info('Installing K3s...');
@@ -172,7 +166,7 @@ class UnderpostCluster {
172
166
  logger.info('Waiting for K3s to be ready...');
173
167
  shellExec(`sudo systemctl is-active --wait k3s || sudo systemctl wait --for=active k3s.service`);
174
168
  logger.info('K3s service is active.');
175
- } else if (options.kubeadm === true) {
169
+ } else if (options.kubeadm) {
176
170
  logger.info('Initializing Kubeadm control plane...');
177
171
  // Set default values if not provided
178
172
  const podNetworkCidr = options.podNetworkCidr || '192.168.0.0/16';
@@ -208,7 +202,7 @@ class UnderpostCluster {
208
202
  logger.info('Initializing Kind cluster...');
209
203
  shellExec(
210
204
  `cd ${underpostRoot}/manifests && kind create cluster --config kind-config${
211
- options?.dev === true ? '-dev' : ''
205
+ options.dev ? '-dev' : ''
212
206
  }.yaml`,
213
207
  );
214
208
  Underpost.cluster.chown('kind'); // Pass 'kind' to chown
@@ -218,14 +212,12 @@ class UnderpostCluster {
218
212
  // --- Optional Component Deployments (Databases, Ingress, Cert-Manager) ---
219
213
  // These deployments happen after the base cluster is up.
220
214
 
221
- if (options.full === true || options.dedicatedGpu === true) {
215
+ if (options.dedicatedGpu) {
222
216
  shellExec(`node ${underpostRoot}/bin/deploy nvidia-gpu-operator`);
223
- shellExec(
224
- `node ${underpostRoot}/bin/deploy kubeflow-spark-operator${options.kubeadm === true ? ' kubeadm' : ''}`,
225
- );
217
+ shellExec(`node ${underpostRoot}/bin/deploy kubeflow-spark-operator${options.kubeadm ? ' kubeadm' : ''}`);
226
218
  }
227
219
 
228
- if (options.grafana === true) {
220
+ if (options.grafana) {
229
221
  shellExec(`kubectl delete deployment grafana -n ${options.namespace} --ignore-not-found`);
230
222
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/grafana -n ${options.namespace}`);
231
223
  const yaml = `${fs
@@ -238,7 +230,7 @@ EOF
238
230
  `);
239
231
  }
240
232
 
241
- if (options.prom && typeof options.prom === 'string') {
233
+ if (options.prom) {
242
234
  shellExec(`kubectl delete deployment prometheus --ignore-not-found`);
243
235
  shellExec(`kubectl delete configmap prometheus-config --ignore-not-found`);
244
236
  shellExec(`kubectl delete service prometheus --ignore-not-found`);
@@ -257,8 +249,8 @@ EOF
257
249
  `);
258
250
  }
259
251
 
260
- if (options.full === true || options.valkey === true) {
261
- if (options.pullImage === true) Underpost.cluster.pullImage('valkey/valkey:latest', options);
252
+ if (options.valkey) {
253
+ if (options.pullImage) Underpost.cluster.pullImage('valkey/valkey:latest', options);
262
254
  shellExec(`kubectl delete statefulset valkey-service -n ${options.namespace} --ignore-not-found`);
263
255
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/valkey -n ${options.namespace}`);
264
256
  await Underpost.test.statusMonitor('valkey-service', 'Running', 'pods', 1000, 60 * 10);
@@ -266,17 +258,17 @@ EOF
266
258
  if (options.ipfs) {
267
259
  await Underpost.ipfs.deploy(options, underpostRoot);
268
260
  }
269
- if (options.full === true || options.mariadb === true) {
261
+ if (options.mariadb) {
270
262
  shellExec(
271
263
  `sudo kubectl create secret generic mariadb-secret --from-file=username=/home/dd/engine/engine-private/mariadb-username --from-file=password=/home/dd/engine/engine-private/mariadb-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
272
264
  );
273
265
  shellExec(`kubectl delete statefulset mariadb-statefulset -n ${options.namespace} --ignore-not-found`);
274
266
 
275
- if (options.pullImage === true) Underpost.cluster.pullImage('mariadb:latest', options);
267
+ if (options.pullImage) Underpost.cluster.pullImage('mariadb:latest', options);
276
268
  shellExec(`kubectl apply -f ${underpostRoot}/manifests/mariadb/storage-class.yaml -n ${options.namespace}`);
277
269
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/mariadb -n ${options.namespace}`);
278
270
  }
279
- if (options.full === true || options.mysql === true) {
271
+ if (options.mysql) {
280
272
  shellExec(
281
273
  `sudo kubectl create secret generic mysql-secret --from-file=username=/home/dd/engine/engine-private/mysql-username --from-file=password=/home/dd/engine/engine-private/mysql-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
282
274
  );
@@ -285,15 +277,15 @@ EOF
285
277
  shellExec(`sudo chown -R $(whoami):$(whoami) /mnt/data`);
286
278
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/mysql -n ${options.namespace}`);
287
279
  }
288
- if (options.full === true || options.postgresql === true) {
289
- if (options.pullImage === true) Underpost.cluster.pullImage('postgres:latest', options);
280
+ if (options.postgresql) {
281
+ if (options.pullImage) Underpost.cluster.pullImage('postgres:latest', options);
290
282
  shellExec(
291
283
  `sudo kubectl create secret generic postgres-secret --from-file=password=/home/dd/engine/engine-private/postgresql-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
292
284
  );
293
285
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/postgresql -n ${options.namespace}`);
294
286
  }
295
- if (options.mongodb4 === true) {
296
- if (options.pullImage === true) Underpost.cluster.pullImage('mongo:4.4', options);
287
+ if (options.mongodb4) {
288
+ if (options.pullImage) Underpost.cluster.pullImage('mongo:4.4', options);
297
289
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb-4.4 -n ${options.namespace}`);
298
290
 
299
291
  const deploymentName = 'mongodb-deployment';
@@ -314,8 +306,8 @@ EOF
314
306
  --eval 'rs.initiate(${JSON.stringify(mongoConfig)})'`,
315
307
  );
316
308
  }
317
- } else if (options.full === true || options.mongodb === true) {
318
- if (options.pullImage === true) Underpost.cluster.pullImage('mongo:latest', options);
309
+ } else if (options.mongodb) {
310
+ if (options.pullImage) Underpost.cluster.pullImage('mongo:latest', options);
319
311
  shellExec(
320
312
  `sudo kubectl create secret generic mongodb-keyfile --from-file=/home/dd/engine/engine-private/mongodb-keyfile --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
321
313
  );
@@ -344,11 +336,11 @@ EOF
344
336
  }
345
337
  }
346
338
 
347
- if (options.full === true || options.contour === true) {
339
+ if (options.contour) {
348
340
  shellExec(
349
341
  `kubectl apply -f https://cdn.jsdelivr.net/gh/projectcontour/contour@release-1.33/examples/render/contour.yaml`,
350
342
  );
351
- if (options.kubeadm === true) {
343
+ if (options.kubeadm) {
352
344
  // Envoy service might need NodePort for kubeadm
353
345
  shellExec(
354
346
  `sudo kubectl apply -f ${underpostRoot}/manifests/envoy-service-nodeport.yaml -n ${options.namespace}`,
@@ -358,7 +350,7 @@ EOF
358
350
  // so a specific NodePort service might not be needed or can be configured differently.
359
351
  }
360
352
 
361
- if (options.full === true || options.certManager === true) {
353
+ if (options.certManager) {
362
354
  if (!Underpost.deploy.get('cert-manager').find((p) => p.STATUS === 'Running')) {
363
355
  shellExec(`helm repo add jetstack https://charts.jetstack.io --force-update`);
364
356
  shellExec(
package/src/cli/index.js CHANGED
@@ -218,7 +218,6 @@ program
218
218
  .option('--contour', 'Initializes the cluster with Project Contour base HTTPProxy and Envoy.')
219
219
  .option('--cert-manager', "Initializes the cluster with a Let's Encrypt production ClusterIssuer.")
220
220
  .option('--dedicated-gpu', 'Initializes the cluster with dedicated GPU base resources and environment settings.')
221
- .option('--full', 'Initializes the cluster with all available statefulsets and services.')
222
221
  .option(
223
222
  '--ns-use <ns-name>',
224
223
  "Switches the current Kubernetes context to the specified namespace (creates if it doesn't exist).",
package/src/cli/run.js CHANGED
@@ -217,6 +217,20 @@ class UnderpostRun {
217
217
  }
218
218
  },
219
219
 
220
+ /**
221
+ * @method ipfs-expose
222
+ * @description Exposes IPFS Cluster services on specified ports for local access.
223
+ * @type {Function}
224
+ * @memberof UnderpostRun
225
+ */
226
+ 'ipfs-expose': (path, options = DEFAULT_OPTION) => {
227
+ const ports = [5001, 9094, 8080];
228
+ for (const port of ports)
229
+ shellExec(`node bin deploy --expose ipfs-cluster --expose-port ${port} --disable-update-underpost-config`, {
230
+ async: true,
231
+ });
232
+ },
233
+
220
234
  /**
221
235
  * @method metadata
222
236
  * @description Generates metadata for the specified path after exposing the development cluster.
@@ -27,8 +27,8 @@ const LoadingAnimation = {
27
27
  ? subThemeManager.darkColor
28
28
  : `#66e400`
29
29
  : subThemeManager.lightColor
30
- ? subThemeManager.lightColor
31
- : `#157e00`};"
30
+ ? subThemeManager.lightColor
31
+ : `#157e00`};"
32
32
  ></div>
33
33
  `,
34
34
  );
@@ -131,7 +131,6 @@ const LoadingAnimation = {
131
131
  if (!backgroundContainer) backgroundContainer = '.ssr-background';
132
132
  if (s(backgroundContainer)) {
133
133
  s(backgroundContainer).style.display = 'none';
134
- if (s(`.main-body-btn-container`)) s(`.main-body-btn-container`).classList.remove('hide');
135
134
  if (callBack) callBack();
136
135
  }
137
136
  },
@@ -322,7 +322,7 @@ const Modal = {
322
322
  'body',
323
323
  html`
324
324
  <div
325
- class="abs main-body-btn-container hide"
325
+ class="abs main-body-btn-container"
326
326
  style="top: ${options.heightTopBar + 50}px; z-index: 9; ${true ||
327
327
  (options.mode && options.mode.match('right'))
328
328
  ? 'right'
package/src/index.js CHANGED
@@ -42,7 +42,7 @@ class Underpost {
42
42
  * @type {String}
43
43
  * @memberof Underpost
44
44
  */
45
- static version = 'v3.0.1';
45
+ static version = 'v3.0.2';
46
46
 
47
47
  /**
48
48
  * Required Node.js major version
@@ -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', true);
102
+ app.set('trust proxy', 1);
103
103
 
104
104
  app.use((req, res, next) => {
105
105
  res.on('finish', () => {
@@ -647,24 +647,24 @@ function applySecurity(app, opts = {}) {
647
647
  }),
648
648
  );
649
649
  logger.info('Cors origin', origin);
650
-
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
-
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
- }