underpost 3.1.3 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/ghpkg.ci.yml +4 -4
  3. package/.github/workflows/npmpkg.ci.yml +28 -11
  4. package/.github/workflows/pwa-microservices-template-page.cd.yml +3 -4
  5. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  6. package/.github/workflows/release.cd.yml +4 -4
  7. package/CHANGELOG.md +324 -1
  8. package/CLI-HELP.md +49 -3
  9. package/README.md +3 -2
  10. package/bin/build.js +18 -12
  11. package/bin/deploy.js +177 -124
  12. package/bin/file.js +3 -0
  13. package/conf.js +3 -2
  14. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  16. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  17. package/manifests/deployment/dd-test-development/deployment.yaml +72 -50
  18. package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
  19. package/manifests/deployment/playwright/deployment.yaml +1 -1
  20. package/nodemon.json +1 -1
  21. package/package.json +22 -15
  22. package/scripts/rhel-grpc-setup.sh +56 -0
  23. package/src/api/file/file.ref.json +18 -0
  24. package/src/api/user/user.service.js +8 -7
  25. package/src/cli/cluster.js +7 -7
  26. package/src/cli/db.js +76 -242
  27. package/src/cli/deploy.js +104 -65
  28. package/src/cli/env.js +1 -0
  29. package/src/cli/fs.js +2 -1
  30. package/src/cli/index.js +42 -1
  31. package/src/cli/kubectl.js +211 -0
  32. package/src/cli/release.js +284 -0
  33. package/src/cli/repository.js +291 -75
  34. package/src/cli/run.js +188 -33
  35. package/src/cli/test.js +3 -3
  36. package/src/client/Default.index.js +3 -4
  37. package/src/client/components/core/AppStore.js +69 -0
  38. package/src/client/components/core/CalendarCore.js +2 -2
  39. package/src/client/components/core/DropDown.js +129 -17
  40. package/src/client/components/core/Keyboard.js +2 -2
  41. package/src/client/components/core/LogIn.js +2 -2
  42. package/src/client/components/core/LogOut.js +2 -2
  43. package/src/client/components/core/Modal.js +0 -1
  44. package/src/client/components/core/Panel.js +0 -1
  45. package/src/client/components/core/PanelForm.js +19 -19
  46. package/src/client/components/core/SocketIo.js +82 -29
  47. package/src/client/components/core/SocketIoHandler.js +75 -0
  48. package/src/client/components/core/Stream.js +143 -95
  49. package/src/client/components/core/Webhook.js +40 -7
  50. package/src/client/components/default/AppStoreDefault.js +5 -0
  51. package/src/client/components/default/LogInDefault.js +3 -3
  52. package/src/client/components/default/LogOutDefault.js +2 -2
  53. package/src/client/components/default/MenuDefault.js +5 -5
  54. package/src/client/components/default/SocketIoDefault.js +3 -51
  55. package/src/client/services/core/core.service.js +20 -8
  56. package/src/client/services/user/user.management.js +2 -2
  57. package/src/index.js +24 -1
  58. package/src/runtime/express/Express.js +18 -1
  59. package/src/runtime/lampp/Dockerfile +9 -2
  60. package/src/runtime/lampp/Lampp.js +4 -3
  61. package/src/runtime/wp/Dockerfile +64 -0
  62. package/src/runtime/wp/Wp.js +497 -0
  63. package/src/server/auth.js +24 -1
  64. package/src/server/backup.js +19 -1
  65. package/src/server/client-build-docs.js +9 -2
  66. package/src/server/client-build.js +31 -31
  67. package/src/server/client-formatted.js +109 -57
  68. package/src/server/ipfs-client.js +24 -1
  69. package/src/server/peer.js +8 -0
  70. package/src/server/runtime.js +25 -1
  71. package/src/server/start.js +6 -0
  72. package/src/ws/IoInterface.js +1 -10
  73. package/src/ws/IoServer.js +14 -33
  74. package/src/ws/core/channels/core.ws.chat.js +65 -20
  75. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  76. package/src/ws/core/channels/core.ws.stream.js +90 -31
  77. package/src/ws/core/core.ws.connection.js +12 -33
  78. package/src/ws/core/core.ws.emit.js +10 -26
  79. package/src/ws/core/core.ws.server.js +25 -58
  80. package/src/ws/default/channels/default.ws.main.js +53 -12
  81. package/src/ws/default/default.ws.connection.js +26 -13
  82. package/src/ws/default/default.ws.server.js +30 -12
  83. package/src/client/components/default/ElementsDefault.js +0 -38
  84. package/src/ws/core/management/core.ws.chat.js +0 -8
  85. package/src/ws/core/management/core.ws.mailer.js +0 -16
  86. package/src/ws/core/management/core.ws.stream.js +0 -8
  87. package/src/ws/default/management/default.ws.main.js +0 -8
package/src/cli/run.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * @namespace UnderpostRun
5
5
  */
6
6
 
7
- import { daemonProcess, getTerminalPid, shellCd, shellExec } from '../server/process.js';
7
+ import { daemonProcess, getTerminalPid, pbcopy, shellCd, shellExec } from '../server/process.js';
8
+ import crypto from 'crypto';
8
9
  import {
9
10
  awaitDeployMonitor,
10
11
  buildKindPorts,
@@ -17,12 +18,31 @@ import {
17
18
  import { actionInitLog, loggerFactory } from '../server/logger.js';
18
19
 
19
20
  import fs from 'fs-extra';
21
+ import net from 'net';
20
22
  import { range, setPad, timer } from '../client/components/core/CommonJs.js';
21
23
 
22
24
  import os from 'os';
23
25
  import Underpost from '../index.js';
24
26
  import dotenv from 'dotenv';
25
27
 
28
+ const waitForPort = (port, host = '127.0.0.1', { maxAttempts = 30, interval = 2000 } = {}) =>
29
+ new Promise((resolve, reject) => {
30
+ let attempts = 0;
31
+ const tryConnect = () => {
32
+ attempts++;
33
+ const socket = net.createConnection({ port, host }, () => {
34
+ socket.destroy();
35
+ resolve();
36
+ });
37
+ socket.on('error', () => {
38
+ socket.destroy();
39
+ if (attempts >= maxAttempts) return reject(new Error(`Port ${port} not ready after ${maxAttempts} attempts`));
40
+ setTimeout(tryConnect, interval);
41
+ });
42
+ };
43
+ tryConnect();
44
+ });
45
+
26
46
  const logger = loggerFactory(import.meta);
27
47
 
28
48
  /**
@@ -154,6 +174,8 @@ const DEFAULT_OPTION = {
154
174
  createJobNow: false,
155
175
  fromNCommit: 0,
156
176
  hostAliases: '',
177
+ gitClean: false,
178
+ copy: false,
157
179
  };
158
180
 
159
181
  /**
@@ -246,8 +268,15 @@ class UnderpostRun {
246
268
  const ports = '6379,27017';
247
269
  shellExec(`node bin run kill '${ports}'`);
248
270
  shellExec(`node bin run dev-cluster --dev --expose --namespace ${options.namespace}`, { async: true });
249
- console.log('Loading fordward services...');
250
- await timer(5000);
271
+ logger.info('Waiting for port-forward services to be ready...');
272
+ try {
273
+ await Promise.all([waitForPort(27017), waitForPort(6379)]);
274
+ logger.info('Port-forward services are ready');
275
+ } catch (err) {
276
+ logger.error('Port-forward services failed to become ready', { error: err.message });
277
+ shellExec(`node bin run kill '${ports}'`);
278
+ throw err;
279
+ }
251
280
  shellExec(`node bin metadata --generate ${path}`);
252
281
  shellExec(`node bin db --dev --clean-fs-collection dd`);
253
282
  shellExec(`node bin run kill '${ports}'`);
@@ -373,8 +402,12 @@ class UnderpostRun {
373
402
  }
374
403
  shellExec(`${baseCommand} run pull`);
375
404
 
376
- // Capture last N commit messages for propagation (default: last 1 commit)
377
- const fromN = options.fromNCommit && parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
405
+ // Capture last N commit messages for propagation.
406
+ // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
407
+ const fromN =
408
+ options.fromNCommit && parseInt(options.fromNCommit) > 0
409
+ ? parseInt(options.fromNCommit)
410
+ : Underpost.repo.getUnpushedCount('.').count;
378
411
  const message = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
379
412
  silent: true,
380
413
  stdout: true,
@@ -423,6 +456,40 @@ class UnderpostRun {
423
456
  });
424
457
  },
425
458
 
459
+ /**
460
+ * @method template-deploy-local
461
+ * @description Similar to `template-deploy` but runs the workflow locally without dispatching GitHub Actions. It pulls the latest changes, pushes to GitHub, builds the template, and optionally triggers a local release with CI push.
462
+ * @param {string} path - The deployment path identifier (e.g., 'sync-engine-core', 'init-engine-core', or empty for build-only).
463
+ * @param {Object} options - The default underpost runner options for customizing workflow
464
+ * @memberof UnderpostRun
465
+ */
466
+ 'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
467
+ const baseCommand = options.dev ? 'node bin' : 'underpost';
468
+ shellExec(`npm run security:secrets`);
469
+ const reportPath = './gitleaks-report.json';
470
+ if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
471
+ logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
472
+ return;
473
+ }
474
+ shellExec(`${baseCommand} run pull`);
475
+
476
+ // Capture last N commit messages from the engine repo.
477
+ // When --from-n-commit is not set, auto-detect unpushed commit count (same as --unpush flag).
478
+ const fromN =
479
+ options.fromNCommit && parseInt(options.fromNCommit) > 0
480
+ ? parseInt(options.fromNCommit)
481
+ : Underpost.repo.getUnpushedCount('.').count;
482
+ const rawMessage = shellExec(`node bin cmt --changelog ${fromN} --changelog-no-hash`, {
483
+ silent: true,
484
+ stdout: true,
485
+ }).trim();
486
+ const sanitizedMessage = Underpost.repo.sanitizeChangelogMessage(rawMessage);
487
+
488
+ const { triggerCmd } = path
489
+ ? await Underpost.release.ci(path, sanitizedMessage, options)
490
+ : await Underpost.release.pwa(sanitizedMessage, options);
491
+ pbcopy(triggerCmd + ' && cd /home/dd/engine');
492
+ },
426
493
  /**
427
494
  * @method template-deploy-image
428
495
  * @description Dispatches the Docker image CI workflow for the `engine` repository.
@@ -438,6 +505,21 @@ class UnderpostRun {
438
505
  inputs: {},
439
506
  });
440
507
  },
508
+ /**
509
+ * @method docker-image
510
+ * @description Dispatches the Docker image CI workflow (`docker-image.ci.yml`) for the `engine` repository via `workflow_dispatch`.
511
+ * @param {string} path - The input value, identifier, or path for the operation.
512
+ * @param {Object} options - The default underpost runner options for customizing workflow
513
+ * @memberof UnderpostRun
514
+ */
515
+ 'docker-image': (path, options = DEFAULT_OPTION) => {
516
+ Underpost.repo.dispatchWorkflow({
517
+ repo: `${process.env.GITHUB_USERNAME}/engine`,
518
+ workflowFile: 'docker-image.ci.yml',
519
+ ref: 'master',
520
+ inputs: {},
521
+ });
522
+ },
441
523
  /**
442
524
  * @method clean
443
525
  * @description Changes directory to the provided path (defaulting to `/home/dd/engine`) and runs `node bin/deploy clean-core-repo`.
@@ -605,20 +687,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
605
687
  const cmdString = options.cmd
606
688
  ? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
607
689
  : '';
690
+ const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
691
+ const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
608
692
 
609
693
  shellExec(
610
- `${baseCommand} deploy --kubeadm --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
694
+ `${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
611
695
  image ? ` --image ${image}` : ''
612
696
  }${versions ? ` --versions ${versions}` : ''}${
613
697
  options.namespace ? ` --namespace ${options.namespace}` : ''
614
- }${timeoutFlags}${cmdString} ${deployId} ${env}`,
698
+ }${timeoutFlags}${cmdString}${gitCleanFlag} ${deployId} ${env}`,
615
699
  );
616
700
 
617
701
  if (isDeployRunnerContext(path, options)) {
618
702
  shellExec(
619
- `${baseCommand} deploy --kubeadm${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
703
+ `${baseCommand} deploy${clusterFlag}${cmdString} --replicas ${replicas} --disable-update-proxy ${deployId} ${env} --versions ${versions}${
620
704
  options.namespace ? ` --namespace ${options.namespace}` : ''
621
- }${timeoutFlags}`,
705
+ }${timeoutFlags}${gitCleanFlag}`,
622
706
  );
623
707
  if (!targetTraffic)
624
708
  targetTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace });
@@ -830,6 +914,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
830
914
  const confInstances = JSON.parse(
831
915
  fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'),
832
916
  );
917
+ let promotedTraffic = '';
833
918
  for (const instance of confInstances) {
834
919
  let {
835
920
  id: _id,
@@ -849,6 +934,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
849
934
  namespace: options.namespace,
850
935
  });
851
936
  const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
937
+ promotedTraffic = targetTraffic;
852
938
  let proxyYaml =
853
939
  Underpost.deploy.baseProxyYamlFactory({ host: _host, env: options.tls ? 'production' : env, options }) +
854
940
  Underpost.deploy.deploymentYamlServiceFactory({
@@ -874,6 +960,18 @@ EOF
874
960
  { disableLog: true },
875
961
  );
876
962
  }
963
+ // Refresh the gRPC service to ensure it points to the parent deploy's current traffic.
964
+ if (promotedTraffic) {
965
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
966
+ const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
967
+ deployId,
968
+ env,
969
+ confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
970
+ namespace: options.namespace,
971
+ traffic: [parentTraffic],
972
+ });
973
+ if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
974
+ }
877
975
  },
878
976
 
879
977
  /**
@@ -913,12 +1011,12 @@ EOF
913
1011
  // `localhost/rockylinux9-underpost:${Underpost.version}`
914
1012
  if (!_image) _image = `underpost/underpost-engine:${Underpost.version}`;
915
1013
 
916
- if (options.nodeName) {
917
- shellExec(`sudo crictl pull ${_image}`);
918
- } else {
919
- shellExec(`docker pull ${_image}`);
920
- shellExec(`sudo kind load docker-image ${_image}`);
921
- }
1014
+ Underpost.image.pullDockerHubImage({
1015
+ dockerhubImage: _image,
1016
+ kind: options.kind || (!options.nodeName && !options.kubeadm && !options.k3s),
1017
+ kubeadm: options.nodeName || options.kubeadm,
1018
+ k3s: options.k3s,
1019
+ });
922
1020
 
923
1021
  const currentTraffic = Underpost.deploy.getCurrentTraffic(_deployId, {
924
1022
  hostTest: _host,
@@ -927,7 +1025,7 @@ EOF
927
1025
 
928
1026
  const targetTraffic = currentTraffic ? (currentTraffic === 'blue' ? 'green' : 'blue') : 'blue';
929
1027
  const podId = `${_deployId}-${env}-${targetTraffic}`;
930
- const ignorePods = Underpost.deploy.get(podId, 'pods', options.namespace).map((p) => p.NAME);
1028
+ const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
931
1029
  Underpost.deploy.configMap(env, options.namespace);
932
1030
  shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
933
1031
  shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
@@ -939,7 +1037,30 @@ EOF
939
1037
  env,
940
1038
  version: targetTraffic,
941
1039
  nodeName: options.nodeName,
1040
+ clusterContext: options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind',
1041
+ gitClean: options.gitClean || false,
942
1042
  });
1043
+ // Regenerate the parent deploy's gRPC ClusterIP service pointing to the
1044
+ // parent's current traffic colour and apply it before the instance pod starts so
1045
+ // DNS is resolvable the moment the pod boots.
1046
+ const parentTraffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace: options.namespace }) || 'blue';
1047
+ const grpcServicePath = Underpost.deploy.buildGrpcServiceManifest({
1048
+ deployId,
1049
+ env,
1050
+ confServer: loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`),
1051
+ namespace: options.namespace,
1052
+ traffic: [targetTraffic],
1053
+ host: _host,
1054
+ });
1055
+ if (grpcServicePath) shellExec(`kubectl apply -f ${grpcServicePath} -n ${options.namespace}`);
1056
+
1057
+ const resolvedCmd = _cmd[env].map((c) =>
1058
+ c.replaceAll(
1059
+ '{{grpc-service-dns}}',
1060
+ `${deployId}-grpc-service-${env}-${parentTraffic}.${options.namespace || 'default'}.svc.cluster.local:50051`,
1061
+ ),
1062
+ );
1063
+
943
1064
  let deploymentYaml = `---
944
1065
  ${Underpost.deploy
945
1066
  .deploymentYamlPartsFactory({
@@ -951,7 +1072,7 @@ ${Underpost.deploy
951
1072
  image: _image,
952
1073
  namespace: options.namespace,
953
1074
  volumes: _volumes,
954
- cmd: _cmd[env],
1075
+ cmd: resolvedCmd,
955
1076
  })
956
1077
  .replace('{{ports}}', buildKindPorts(_fromPort, _toPort))}
957
1078
  `;
@@ -997,7 +1118,7 @@ EOF
997
1118
  * @memberof UnderpostRun
998
1119
  */
999
1120
  'ls-deployments': async (path, options = DEFAULT_OPTION) => {
1000
- console.table(await Underpost.deploy.get(path, 'deployments', options.namespace));
1121
+ console.table(await Underpost.kubectl.get(path, 'deployments', options.namespace));
1001
1122
  },
1002
1123
 
1003
1124
  /**
@@ -1091,15 +1212,13 @@ EOF
1091
1212
  'db-client': async (path, options = DEFAULT_OPTION) => {
1092
1213
  const { underpostRoot } = options;
1093
1214
 
1094
- const image = 'adminer:4.7.6-standalone';
1095
-
1096
- if (!options.kubeadm && !options.k3s) {
1097
- // Only load if not kubeadm/k3s (Kind needs it)
1098
- shellExec(`docker pull ${image}`);
1099
- shellExec(`sudo kind load docker-image ${image}`);
1100
- } else if (options.kubeadm || options.k3s)
1101
- // For kubeadm/k3s, ensure it's available for containerd
1102
- shellExec(`sudo crictl pull ${image}`);
1215
+ Underpost.image.pullDockerHubImage({
1216
+ dockerhubImage: 'adminer',
1217
+ version: '4.7.6-standalone',
1218
+ kind: options.kind,
1219
+ kubeadm: options.kubeadm,
1220
+ k3s: options.k3s,
1221
+ });
1103
1222
 
1104
1223
  shellExec(`kubectl delete deployment adminer -n ${options.namespace} --ignore-not-found`);
1105
1224
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/deployment/adminer/. -n ${options.namespace}`);
@@ -1693,7 +1812,7 @@ EOF
1693
1812
 
1694
1813
  const { close } = await (async () => {
1695
1814
  const checkAwaitPath = '/await';
1696
- while (!Underpost.deploy.existsContainerFile({ podName, path: checkAwaitPath })) {
1815
+ while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
1697
1816
  logger.info('monitor', checkAwaitPath);
1698
1817
  await timer(1000);
1699
1818
  }
@@ -1720,7 +1839,7 @@ EOF
1720
1839
  logger.info('monitor', checkPath);
1721
1840
  {
1722
1841
  const checkAwaitPath = `/home/dd/docs${checkPath}`;
1723
- while (!Underpost.deploy.existsContainerFile({ podName, path: checkAwaitPath })) {
1842
+ while (!Underpost.kubectl.existsFile({ podName, path: checkAwaitPath })) {
1724
1843
  logger.info('waiting for', checkAwaitPath);
1725
1844
  await timer(1000);
1726
1845
  }
@@ -1786,7 +1905,8 @@ EOF
1786
1905
 
1787
1906
  shellCd(dir);
1788
1907
 
1789
- shellExec(`git init && git add . && git commit -m "Base implementation"`);
1908
+ Underpost.repo.initLocalRepo({ path: dir });
1909
+ shellExec(`git add . && git commit -m "Base implementation"`);
1790
1910
  shellExec(`chmod +x ./replace_params.sh`);
1791
1911
  shellExec(`chmod +x ./build.sh`);
1792
1912
 
@@ -1831,11 +1951,46 @@ EOF
1831
1951
  * @param {Object} options - The default underpost runner options for customizing workflow
1832
1952
  * @memberof UnderpostRun
1833
1953
  */
1954
+ /**
1955
+ * @method generate-pass
1956
+ * @description Generates a cryptographically secure random password that satisfies all validatePassword
1957
+ * constraints (lowercase, uppercase, digit, special char, min 8 chars). Logs the plain password
1958
+ * to the console or, when `--copy` is set, copies it to the clipboard via pbcopy.
1959
+ * @param {string} path - Optional password length (default: 16).
1960
+ * @param {Object} options - The default underpost runner options for customizing workflow.
1961
+ * @param {boolean} options.copy - When true, copies to clipboard instead of logging.
1962
+ * @memberof UnderpostRun
1963
+ */
1964
+ 'generate-pass': (path, options = DEFAULT_OPTION) => {
1965
+ const length = path && parseInt(path) > 0 ? parseInt(path) : 16;
1966
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
1967
+ const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
1968
+ const digits = '0123456789';
1969
+ const special = '@#$%^&*()_+';
1970
+ const all = lower + upper + digits + special;
1971
+ const buf = crypto.randomBytes(length + 4);
1972
+ // Guarantee at least one character from each required class
1973
+ const chars = [
1974
+ lower[buf[0] % lower.length],
1975
+ upper[buf[1] % upper.length],
1976
+ digits[buf[2] % digits.length],
1977
+ special[buf[3] % special.length],
1978
+ ];
1979
+ for (let i = 4; i < length; i++) chars.push(all[buf[i] % all.length]);
1980
+ // Fisher-Yates shuffle using an independent random buffer
1981
+ const shuf = crypto.randomBytes(length);
1982
+ for (let i = chars.length - 1; i > 0; i--) {
1983
+ const j = shuf[i % shuf.length] % (i + 1);
1984
+ [chars[i], chars[j]] = [chars[j], chars[i]];
1985
+ }
1986
+ const password = chars.join('');
1987
+ if (options.copy) pbcopy(password);
1988
+ else console.log(password);
1989
+ },
1990
+
1834
1991
  secret: (path, options = DEFAULT_OPTION) => {
1835
1992
  const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
1836
- const command = options.dev
1837
- ? `node bin secret underpost --create-from-file ${secretPath}`
1838
- : `underpost secret underpost --create-from-file ${secretPath}`;
1993
+ const command = `node bin secret underpost --create-from-file ${secretPath}`;
1839
1994
  shellExec(command);
1840
1995
  },
1841
1996
  /**
package/src/cli/test.js CHANGED
@@ -81,7 +81,7 @@ class UnderpostTest {
81
81
  return await Underpost.test.statusMonitor(options.podName, options.podStatus, options.kindType);
82
82
 
83
83
  if (options.sh === true || options.logs === true) {
84
- const [pod] = Underpost.deploy.get(deployList);
84
+ const [pod] = Underpost.kubectl.get(deployList);
85
85
  if (pod) {
86
86
  if (options.sh) return pbcopy(`sudo kubectl exec -it ${pod.NAME} -- sh`);
87
87
  if (options.logs) return shellExec(`sudo kubectl logs -f ${pod.NAME}`);
@@ -115,7 +115,7 @@ class UnderpostTest {
115
115
  break;
116
116
  }
117
117
  else {
118
- const pods = Underpost.deploy.get(deployId);
118
+ const pods = Underpost.kubectl.get(deployId);
119
119
  if (pods.length > 0)
120
120
  for (const deployData of pods) {
121
121
  const { NAME } = deployData;
@@ -145,7 +145,7 @@ class UnderpostTest {
145
145
  logger.info(`Loading instance`, { podName, status, kindType, deltaMs, maxAttempts });
146
146
  const _monitor = async () => {
147
147
  await timer(deltaMs);
148
- const pods = Underpost.deploy.get(podName, kindType);
148
+ const pods = Underpost.kubectl.get(podName, kindType);
149
149
  let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
150
150
  logger.info(
151
151
  `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
@@ -11,10 +11,9 @@ import { RouterDefault } from './components/default/RoutesDefault.js';
11
11
  import { TranslateDefault } from './components/default/TranslateDefault.js';
12
12
  import { Worker } from './components/core/Worker.js';
13
13
  import { Keyboard } from './components/core/Keyboard.js';
14
- import { DefaultParams } from './components/default/CommonDefault.js';
15
14
  import { SocketIo } from './components/core/SocketIo.js';
16
15
  import { SocketIoDefault } from './components/default/SocketIoDefault.js';
17
- import { ElementsDefault } from './components/default/ElementsDefault.js';
16
+ import { AppStoreDefault } from './components/default/AppStoreDefault.js';
18
17
  import { CssDefaultDark, CssDefaultLight } from './components/default/CssDefault.js';
19
18
  import { EventsUI } from './components/core/EventsUI.js';
20
19
  import { Modal } from './components/core/Modal.js';
@@ -71,14 +70,14 @@ window.onload = () =>
71
70
  await Responsive.Init();
72
71
  await MenuDefault.Render({ htmlMainBody });
73
72
  await SocketIo.Init({
74
- channels: ElementsDefault.Data,
73
+ channels: AppStoreDefault.Data,
75
74
  path: `/`,
76
75
  });
77
76
  await SocketIoDefault.Init();
78
77
  await LogInDefault();
79
78
  await LogOutDefault();
80
79
  await SignUpDefault();
81
- await Keyboard.Init({ callBackTime: DefaultParams.EVENT_CALLBACK_TIME });
80
+ await Keyboard.Init();
82
81
  await Modal.RenderSeoSanitizer();
83
82
  },
84
83
  });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Core per-app state store for WebSocket channel data.
3
+ *
4
+ * @module client/core/AppStore
5
+ * @namespace AppStore
6
+ */
7
+
8
+ /**
9
+ * @class AppStore
10
+ * @classdesc Per-app singleton state store for WebSocket channel data and authenticated user state.
11
+ *
12
+ * Usage: `AppStoreX.Data.user.main.model.user` — the authenticated user object.
13
+ * `AppStoreX.Data` keys (`chat`, `mailer`, `stream`, etc.) — channel definitions for `SocketIo.Init`.
14
+ * @memberof AppStore
15
+ */
16
+ class AppStore {
17
+ /**
18
+ * Channel data map, keyed by channel name (e.g. `user`, `chat`, `mailer`).
19
+ * The `user` channel always contains `{ main: { model: { user: { _id: '' } } } }`.
20
+ *
21
+ * @type {Object.<string, Object>}
22
+ */
23
+ Data;
24
+
25
+ /** @private @type {function(): Object} */
26
+ #initialStateFactory;
27
+
28
+ /**
29
+ * Creates a new AppStore instance.
30
+ *
31
+ * @param {function(): Object} initialStateFactory - Factory function returning the initial data shape.
32
+ * Must return at least `{ user: { main: { model: { user: { _id: '' } } } } }`.
33
+ */
34
+ constructor(initialStateFactory) {
35
+ this.#initialStateFactory = initialStateFactory;
36
+ this.Data = initialStateFactory();
37
+ }
38
+
39
+ /**
40
+ * Resets `Data` to its initial state.
41
+ *
42
+ * @returns {void}
43
+ */
44
+ reset() {
45
+ this.Data = this.#initialStateFactory();
46
+ }
47
+
48
+ /**
49
+ * Creates an AppStore with the standard channel layout.
50
+ * Always includes `user`, `chat`, and `mailer` channels.
51
+ *
52
+ * @static
53
+ * @param {...string} extraChannels - Additional channel names (e.g. `'stream'`).
54
+ * @returns {AppStore}
55
+ */
56
+ static create(...extraChannels) {
57
+ return new AppStore(() => {
58
+ const state = {
59
+ user: { main: { model: { user: { _id: '' } } } },
60
+ chat: {},
61
+ mailer: {},
62
+ };
63
+ for (const ch of extraChannels) state[ch] = {};
64
+ return state;
65
+ });
66
+ }
67
+ }
68
+
69
+ export { AppStore };
@@ -25,7 +25,7 @@ const eventDateFactory = (event) =>
25
25
  const CalendarCore = {
26
26
  RenderStyle: async function () {},
27
27
  Data: {},
28
- Render: async function (options = { idModal: '', Elements: {}, hiddenDates: [] }) {
28
+ Render: async function (options = { idModal: '', appStore: {}, hiddenDates: [] }) {
29
29
  this.Data[options.idModal] = {
30
30
  data: [],
31
31
  originData: [],
@@ -49,7 +49,7 @@ const CalendarCore = {
49
49
  this.Data[options.idModal].filesData = [];
50
50
  this.Data[options.idModal].originData = newInstance(resultData);
51
51
  this.Data[options.idModal].data = resultData.map((o) => {
52
- if (o.creatorUserId && options.Elements.Data.user.main.model.user._id === o.creatorUserId) o.tools = true;
52
+ if (o.creatorUserId && options.appStore.Data.user.main.model.user._id === o.creatorUserId) o.tools = true;
53
53
  o.id = o._id;
54
54
 
55
55
  this.Data[options.idModal].filesData.push({});