underpost 3.0.0 → 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.
@@ -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/lxd.js CHANGED
@@ -344,7 +344,7 @@ ipv6.address=none`);
344
344
  }
345
345
  case 'dev-reset': {
346
346
  shellExec(
347
- `lxc exec ${vmName} -- bash -lc 'cd /home/dd/engine && node bin cluster --dev --reset && node bin cluster --dev --k3s'`,
347
+ `lxc exec ${vmName} -- bash -lc 'cd /home/dd/engine && node bin cluster --dev --reset --k3s && node bin cluster --dev --k3s'`,
348
348
  );
349
349
  break;
350
350
  }
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.
@@ -1148,7 +1162,7 @@ EOF
1148
1162
  const baseClusterCommand = options.dev ? ' --dev' : '';
1149
1163
  const clusterType = options.k3s ? 'k3s' : 'kubeadm';
1150
1164
  shellCd(`/home/dd/engine`);
1151
- shellExec(`${baseCommand} cluster${baseClusterCommand} --reset`);
1165
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --reset --${clusterType}`);
1152
1166
  await timer(5000);
1153
1167
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
1154
1168
  await timer(5000);
@@ -1779,8 +1793,9 @@ EOF
1779
1793
  * @memberof UnderpostRun
1780
1794
  */
1781
1795
  'gpu-env': (path, options = DEFAULT_OPTION) => {
1796
+ const clusterType = 'kubeadm';
1782
1797
  shellExec(
1783
- `node bin cluster --dev --reset && node bin cluster --dev --dedicated-gpu --kubeadm && kubectl get pods --all-namespaces -o wide -w`,
1798
+ `node bin cluster --dev --reset --${clusterType} && node bin cluster --dev --dedicated-gpu --${clusterType} && kubectl get pods --all-namespaces -o wide -w`,
1784
1799
  );
1785
1800
  },
1786
1801
  /**
@@ -1,9 +1,9 @@
1
1
  import { Badge } from './Badge.js';
2
2
  import { BtnIcon } from './BtnIcon.js';
3
- import { Css, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
3
+ import { Css, darkTheme, renderCssAttr, simpleIconsRender, ThemeEvents, Themes } from './Css.js';
4
4
  import { buildBadgeToolTipMenuOption, Modal, renderViewTitle } from './Modal.js';
5
5
  import { listenQueryPathInstance, setQueryPath, closeModalRouteChangeEvent, getProxyPath } from './Router.js';
6
- import { htmls, s } from './VanillaJs.js';
6
+ import { htmls, s, sIframe } from './VanillaJs.js';
7
7
 
8
8
  // https://mintlify.com/docs/quickstart
9
9
 
@@ -39,6 +39,7 @@ const Docs = {
39
39
  RouterInstance: Modal.Data['modal-docs'].options.RouterInstance,
40
40
  });
41
41
  const iframeEl = s(`.iframe-${ModalId}`);
42
+ let swaggerThemeEventKey = null;
42
43
  if (iframeEl) {
43
44
  iframeEl.addEventListener('load', () => {
44
45
  try {
@@ -51,7 +52,95 @@ const Docs = {
51
52
  // cross-origin or security restriction — safe to ignore
52
53
  }
53
54
  window.scrollTo(0, 0);
55
+ // Bind Shift+K inside the iframe to focus the parent SearchBox (mirrors app-wide shortcut)
56
+ try {
57
+ const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;
58
+ if (iframeDoc) {
59
+ iframeDoc.addEventListener('keydown', (e) => {
60
+ if (e.shiftKey && e.key.toLowerCase() === 'k') {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ if (s(`.top-bar-search-box`)) {
64
+ if (s(`.main-body-btn-ui-close`) && s(`.main-body-btn-ui-close`).classList.contains('hide')) {
65
+ s(`.main-body-btn-ui-open`).click();
66
+ }
67
+ s(`.top-bar-search-box`).blur();
68
+ s(`.top-bar-search-box`).focus();
69
+ s(`.top-bar-search-box`).select();
70
+ }
71
+ }
72
+ });
73
+ }
74
+ } catch (e) {
75
+ // cross-origin or security restriction — safe to ignore
76
+ }
54
77
  });
78
+
79
+ if (type === 'src') {
80
+ swaggerThemeEventKey = `jsdocs-iframe-${ModalId}`;
81
+
82
+ const applyJsDocsTheme = (isDark) => {
83
+ try {
84
+ const iframeWin = iframeEl.contentWindow;
85
+ if (!iframeWin) return;
86
+ const theme = isDark ? 'dark' : 'light';
87
+ if (typeof iframeWin.updateTheme === 'function') {
88
+ // clean-jsdoc-theme exposes updateTheme() globally
89
+ iframeWin.updateTheme(theme);
90
+ } else {
91
+ // Fallback: replicate localUpdateTheme manually
92
+ const iframeDoc = iframeEl.contentDocument || iframeWin.document;
93
+ if (!iframeDoc || !iframeDoc.body) return;
94
+ iframeDoc.body.setAttribute('data-theme', theme);
95
+ iframeDoc.body.classList.remove('dark', 'light');
96
+ iframeDoc.body.classList.add(theme);
97
+ const iconID = isDark ? '#light-theme-icon' : '#dark-theme-icon';
98
+ const svgUses = sIframe(iframeEl, '.theme-svg-use') ? iframeDoc.querySelectorAll('.theme-svg-use') : [];
99
+ svgUses.forEach((svg) => svg.setAttribute('xlink:href', iconID));
100
+ iframeWin.localStorage?.setItem('theme', theme);
101
+ }
102
+ } catch (e) {
103
+ // cross-origin or security restriction — safe to ignore
104
+ }
105
+ };
106
+
107
+ // Apply current theme as soon as the iframe content is ready
108
+ iframeEl.addEventListener('load', () => applyJsDocsTheme(darkTheme));
109
+
110
+ // Keep in sync whenever the parent page theme changes
111
+ ThemeEvents[swaggerThemeEventKey] = () => {
112
+ if (s(`.iframe-${ModalId}`)) applyJsDocsTheme(darkTheme);
113
+ };
114
+ }
115
+
116
+ if (type === 'api') {
117
+ swaggerThemeEventKey = `swagger-iframe-${ModalId}`;
118
+
119
+ const applySwaggerTheme = (isDark) => {
120
+ try {
121
+ const iframeDoc = iframeEl.contentDocument || iframeEl.contentWindow?.document;
122
+ if (!iframeDoc || !iframeDoc.body) return;
123
+ if (isDark) {
124
+ iframeDoc.body.classList.add('swagger-dark');
125
+ } else {
126
+ iframeDoc.body.classList.remove('swagger-dark');
127
+ }
128
+ iframeEl.contentWindow?.localStorage?.setItem('swagger-theme', isDark ? 'dark' : 'light');
129
+ const toggleBtn = sIframe(iframeEl, '#swagger-theme-toggle');
130
+ if (toggleBtn) toggleBtn.textContent = isDark ? '\u2600\uFE0F Light Mode' : '\uD83C\uDF19 Dark Mode';
131
+ } catch (e) {
132
+ // cross-origin or security restriction — safe to ignore
133
+ }
134
+ };
135
+
136
+ // Apply current theme as soon as the iframe content is ready
137
+ iframeEl.addEventListener('load', () => applySwaggerTheme(darkTheme));
138
+
139
+ // Keep in sync whenever the parent page theme changes
140
+ ThemeEvents[swaggerThemeEventKey] = () => {
141
+ if (s(`.iframe-${ModalId}`)) applySwaggerTheme(darkTheme);
142
+ };
143
+ }
55
144
  }
56
145
  Modal.Data[ModalId].onObserverListener[ModalId] = () => {
57
146
  if (s(`.iframe-${ModalId}`)) {
@@ -67,6 +156,7 @@ const Docs = {
67
156
  };
68
157
  Modal.Data[ModalId].onObserverListener[ModalId]();
69
158
  Modal.Data[ModalId].onCloseListener[ModalId] = () => {
159
+ if (swaggerThemeEventKey) delete ThemeEvents[swaggerThemeEventKey];
70
160
  closeModalRouteChangeEvent({ closedId: ModalId });
71
161
  };
72
162
  },
@@ -342,10 +432,6 @@ const Docs = {
342
432
  <div class="docs-landing">
343
433
  <div class="docs-header">
344
434
  <h1>Documentation</h1>
345
- <p>
346
- Find everything you need to build amazing applications with our platform. Get started with our guides, API
347
- reference, and examples.
348
- </p>
349
435
  <!--
350
436
  <div class="search-bar">
351
437
  <i class="fas fa-search"></i>
@@ -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'
@@ -418,12 +418,48 @@ function hexToRgbA(hex) {
418
418
 
419
419
  const htmlStrSanitize = (str) => (str ? str.replace(/<\/?[^>]+(>|$)/g, '').trim() : '');
420
420
 
421
+ /**
422
+ * Query selector inside an iframe. Allows obtaining a single element that is
423
+ * inside an iframe in order to execute events on it.
424
+ * Note: the iframe must be same-origin for this to work.
425
+ *
426
+ * @param {string|Element} iframeEl The CSS selector string or the iframe Element itself.
427
+ * @param {string} el The query selector for the element inside the iframe.
428
+ * @returns {Element|null} The matched element inside the iframe, or null if not found.
429
+ * @memberof VanillaJS
430
+ */
431
+ const sIframe = (iframeEl, el) => {
432
+ const iframe = typeof iframeEl === 'string' ? s(iframeEl) : iframeEl;
433
+ if (!iframe) return null;
434
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
435
+ return iframeDoc ? iframeDoc.querySelector(el) : null;
436
+ };
437
+
438
+ /**
439
+ * Query selector all inside an iframe. Allows obtaining all elements matching a
440
+ * selector that are inside an iframe in order to execute events on them.
441
+ * Note: the iframe must be same-origin for this to work.
442
+ *
443
+ * @param {string|Element} iframeEl The CSS selector string or the iframe Element itself.
444
+ * @param {string} el The query selector for the elements inside the iframe.
445
+ * @returns {NodeList|null} A NodeList of matched elements inside the iframe, or null if not found.
446
+ * @memberof VanillaJS
447
+ */
448
+ const saIframe = (iframeEl, el) => {
449
+ const iframe = typeof iframeEl === 'string' ? s(iframeEl) : iframeEl;
450
+ if (!iframe) return null;
451
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
452
+ return iframeDoc ? iframeDoc.querySelectorAll(el) : null;
453
+ };
454
+
421
455
  export {
422
456
  s,
423
457
  htmls,
424
458
  append,
425
459
  prepend,
426
460
  sa,
461
+ sIframe,
462
+ saIframe,
427
463
  copyData,
428
464
  pasteData,
429
465
  preHTML,
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.0';
45
+ static version = 'v3.0.2';
46
46
 
47
47
  /**
48
48
  * Required Node.js major version
@@ -20,6 +20,7 @@ import { createPeerServer } from '../../server/peer.js';
20
20
  import { createValkeyConnection } from '../../server/valkey.js';
21
21
  import { applySecurity, authMiddlewareFactory } from '../../server/auth.js';
22
22
  import { ssrMiddlewareFactory } from '../../server/ssr.js';
23
+ import { buildSwaggerUiOptions } from '../../server/client-build-docs.js';
23
24
 
24
25
  import { shellExec } from '../../server/process.js';
25
26
  import { devProxyHostFactory, isDevProxyContext, isTlsDevProxy } from '../../server/conf.js';
@@ -98,7 +99,7 @@ class ExpressService {
98
99
 
99
100
  if (origins && isDevProxyContext())
100
101
  origins.push(devProxyHostFactory({ host, includeHttp: true, tls: isTlsDevProxy() }));
101
- app.set('trust proxy', true);
102
+ app.set('trust proxy', 1);
102
103
 
103
104
  app.use((req, res, next) => {
104
105
  res.on('finish', () => {
@@ -167,8 +168,8 @@ class ExpressService {
167
168
  // Swagger UI setup
168
169
  if (fs.existsSync(swaggerJsonPath)) {
169
170
  const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
170
- // Reusing swaggerPath defined outside, removing unnecessary redeclaration
171
- app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc));
171
+ const swaggerUiOptions = await buildSwaggerUiOptions();
172
+ app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc, swaggerUiOptions));
172
173
  }
173
174
 
174
175
  // Security and CORS
@@ -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
  }