cyberia 3.2.22 → 3.2.70
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/.env.example +127 -68
- package/.github/workflows/cyberia-client.cd.yml +40 -0
- package/.github/workflows/cyberia-server.cd.yml +40 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
- package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
- package/.github/workflows/engine-cyberia.cd.yml +33 -24
- package/.github/workflows/engine-cyberia.ci.yml +13 -3
- package/.github/workflows/ghpkg.ci.yml +88 -1
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/hardhat.ci.yml +1 -1
- package/.github/workflows/npmpkg.ci.yml +10 -7
- package/.github/workflows/publish.ci.yml +2 -2
- package/.github/workflows/publish.cyberia.ci.yml +5 -16
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/CHANGELOG.md +301 -1
- package/CLI-HELP.md +71 -6
- package/Dockerfile +141 -43
- package/Dockerfile.dev +143 -0
- package/Dockerfile.test +165 -0
- package/README.md +1 -1
- package/baremetal/commission-workflows.json +1 -0
- package/bin/build.js +31 -0
- package/bin/cyberia.js +1080 -184
- package/bin/deploy.js +2 -2
- package/bin/index.js +1080 -184
- package/bump.config.js +1 -0
- package/compose.env +131 -0
- package/conf.js +18 -1
- package/deployment.yaml +2 -2
- package/docker-compose.yml +316 -0
- package/hardhat/hardhat.config.js +2 -2
- package/hardhat/package-lock.json +620 -2041
- package/hardhat/package.json +7 -5
- package/hardhat/scripts/deployObjectLayerToken.js +18 -18
- package/hardhat/test/ObjectLayerToken.js +378 -274
- package/ipfs/configure-ipfs.sh +13 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/mongodb/entrypoint.sh +76 -0
- package/nginx.conf +87 -0
- package/package.json +31 -19
- package/pv-pvc.yaml +32 -0
- package/scripts/disk-clean.sh +85 -60
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/rocky-kickstart.sh +877 -185
- package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
- package/scripts/test-monitor.sh +3 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
- package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
- package/src/api/cyberia-action/cyberia-action.model.js +21 -29
- package/src/api/cyberia-action/cyberia-action.router.js +42 -7
- package/src/api/cyberia-action/cyberia-action.service.js +20 -4
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
- package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
- package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
- package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
- package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
- package/src/api/cyberia-map/cyberia-map.router.js +21 -6
- package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
- package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
- package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
- package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
- package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
- package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -129
- package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
- package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
- package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
- package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
- package/src/api/ipfs/ipfs.service.js +28 -15
- package/src/api/object-layer/object-layer.router.js +25 -15
- package/src/api/object-layer/object-layer.service.js +15 -20
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
- package/src/cli/baremetal.js +717 -16
- package/src/cli/cluster.js +80 -5
- package/src/cli/deploy.js +127 -11
- package/src/cli/docker-compose.js +648 -0
- package/src/cli/env.js +11 -2
- package/src/cli/fs.js +75 -30
- package/src/cli/image.js +129 -51
- package/src/cli/index.js +110 -6
- package/src/cli/kickstart.js +142 -20
- package/src/cli/release.js +46 -4
- package/src/cli/repository.js +238 -33
- package/src/cli/run.js +520 -114
- package/src/cli/secrets.js +82 -48
- package/src/cli/ssh.js +234 -0
- package/src/cli/static.js +2 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
- package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
- package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
- package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +45 -27
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
- package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +8 -1
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +5 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
- package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
- package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
- package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -6
- package/src/{server → client-builder}/client-build-docs.js +15 -5
- package/src/{server → client-builder}/client-build-live.js +3 -3
- package/src/{server → client-builder}/client-build.js +25 -22
- package/src/{server → client-builder}/client-dev-server.js +3 -3
- package/src/{server → client-builder}/client-icons.js +2 -2
- package/src/{server → client-builder}/ssr.js +5 -5
- package/src/client.build.js +1 -3
- package/src/client.dev.js +1 -1
- package/src/db/mongo/MongoBootstrap.js +12 -12
- package/src/grpc/cyberia/grpc-server.js +255 -70
- package/src/index.js +12 -1
- package/src/mailer/EmailRender.js +1 -1
- package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
- package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
- package/src/projects/cyberia/catalog-cyberia.js +81 -0
- package/src/projects/cyberia/gemini-client.js +175 -0
- package/src/projects/cyberia/generate-saga.js +2107 -0
- package/src/{server → projects/cyberia}/ipfs-client.js +2 -2
- package/src/{server → projects/cyberia}/object-layer.js +12 -108
- package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
- package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
- package/src/{server → projects/cyberia}/shape-generator.js +2 -2
- package/src/{server → projects/underpost}/catalog-underpost.js +1 -2
- package/src/runtime/cyberia-client/Dockerfile +27 -61
- package/src/runtime/cyberia-client/Dockerfile.dev +20 -12
- package/src/runtime/cyberia-server/Dockerfile +21 -20
- package/src/runtime/cyberia-server/Dockerfile.dev +17 -8
- package/src/runtime/engine-cyberia/Dockerfile +143 -0
- package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
- package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
- package/src/runtime/engine-cyberia/compose.env +131 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
- package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
- package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
- package/src/runtime/engine-cyberia/nginx.conf +87 -0
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/server/catalog.js +9 -14
- package/src/server/conf.js +3 -6
- package/src/server/runtime-status.js +18 -1
- package/src/server/start.js +12 -2
- package/src/server.js +6 -2
- package/test/cyberia-instance-conf-defaults.test.js +140 -0
- package/test/deploy-monitor.test.js +26 -10
- package/test/shape-generator.test.js +7 -1
- package/typedoc.dd-cyberia.json +3 -1
- package/typedoc.json +3 -1
- /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
- /package/src/{server → client-builder}/client-formatted.js +0 -0
package/src/cli/secrets.js
CHANGED
|
@@ -13,6 +13,58 @@ import { loggerFactory } from '../server/logger.js';
|
|
|
13
13
|
|
|
14
14
|
const logger = loggerFactory(import.meta);
|
|
15
15
|
|
|
16
|
+
// Shell/runtime-critical and Kubernetes-injected env keys that must never be persisted as
|
|
17
|
+
// application secrets nor injected into a pod via `envFrom`. An injected PATH (or HOME, etc.)
|
|
18
|
+
// overrides the container image's own and breaks coreutils/sudo resolution inside the pod
|
|
19
|
+
// ("rm: command not found"). Single source of truth for both container-env capture and the
|
|
20
|
+
// `underpost-config` secret built from an env file.
|
|
21
|
+
const RESERVED_ENV_KEYS = new Set([
|
|
22
|
+
'HOME',
|
|
23
|
+
'HOSTNAME',
|
|
24
|
+
'PATH',
|
|
25
|
+
'TERM',
|
|
26
|
+
'SHLVL',
|
|
27
|
+
'PWD',
|
|
28
|
+
'_',
|
|
29
|
+
'LANG',
|
|
30
|
+
'LANGUAGE',
|
|
31
|
+
'LC_ALL',
|
|
32
|
+
'container',
|
|
33
|
+
'SHELL',
|
|
34
|
+
'USER',
|
|
35
|
+
'LOGNAME',
|
|
36
|
+
'MAIL',
|
|
37
|
+
'OLDPWD',
|
|
38
|
+
'LESSOPEN',
|
|
39
|
+
'LESSCLOSE',
|
|
40
|
+
'LS_COLORS',
|
|
41
|
+
'DISPLAY',
|
|
42
|
+
'COLORTERM',
|
|
43
|
+
'EDITOR',
|
|
44
|
+
'VISUAL',
|
|
45
|
+
'TERM_PROGRAM',
|
|
46
|
+
'TERM_PROGRAM_VERSION',
|
|
47
|
+
'SSH_AUTH_SOCK',
|
|
48
|
+
'SSH_CLIENT',
|
|
49
|
+
'SSH_CONNECTION',
|
|
50
|
+
'SSH_TTY',
|
|
51
|
+
'XDG_SESSION_ID',
|
|
52
|
+
'XDG_RUNTIME_DIR',
|
|
53
|
+
'XDG_DATA_DIRS',
|
|
54
|
+
'XDG_CONFIG_DIRS',
|
|
55
|
+
'DBUS_SESSION_BUS_ADDRESS',
|
|
56
|
+
'GPG_AGENT_INFO',
|
|
57
|
+
'WINDOWID',
|
|
58
|
+
'DESKTOP_SESSION',
|
|
59
|
+
'SESSION_MANAGER',
|
|
60
|
+
'XAUTHORITY',
|
|
61
|
+
'WAYLAND_DISPLAY',
|
|
62
|
+
'which_declare',
|
|
63
|
+
]);
|
|
64
|
+
const RESERVED_ENV_KEY_PREFIXES = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
65
|
+
const isReservedEnvKey = (key) =>
|
|
66
|
+
RESERVED_ENV_KEYS.has(key) || RESERVED_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
67
|
+
|
|
16
68
|
/**
|
|
17
69
|
* @class UnderpostSecret
|
|
18
70
|
* @description Manages the secrets of the application.
|
|
@@ -49,67 +101,49 @@ class UnderpostSecret {
|
|
|
49
101
|
*/
|
|
50
102
|
createFromContainerEnv() {
|
|
51
103
|
Underpost.env.clean();
|
|
52
|
-
const systemKeys = new Set([
|
|
53
|
-
'HOME',
|
|
54
|
-
'HOSTNAME',
|
|
55
|
-
'PATH',
|
|
56
|
-
'TERM',
|
|
57
|
-
'SHLVL',
|
|
58
|
-
'PWD',
|
|
59
|
-
'_',
|
|
60
|
-
'LANG',
|
|
61
|
-
'LANGUAGE',
|
|
62
|
-
'LC_ALL',
|
|
63
|
-
'container',
|
|
64
|
-
'SHELL',
|
|
65
|
-
'USER',
|
|
66
|
-
'LOGNAME',
|
|
67
|
-
'MAIL',
|
|
68
|
-
'OLDPWD',
|
|
69
|
-
'LESSOPEN',
|
|
70
|
-
'LESSCLOSE',
|
|
71
|
-
'LS_COLORS',
|
|
72
|
-
'DISPLAY',
|
|
73
|
-
'COLORTERM',
|
|
74
|
-
'EDITOR',
|
|
75
|
-
'VISUAL',
|
|
76
|
-
'TERM_PROGRAM',
|
|
77
|
-
'TERM_PROGRAM_VERSION',
|
|
78
|
-
'SSH_AUTH_SOCK',
|
|
79
|
-
'SSH_CLIENT',
|
|
80
|
-
'SSH_CONNECTION',
|
|
81
|
-
'SSH_TTY',
|
|
82
|
-
'XDG_SESSION_ID',
|
|
83
|
-
'XDG_RUNTIME_DIR',
|
|
84
|
-
'XDG_DATA_DIRS',
|
|
85
|
-
'XDG_CONFIG_DIRS',
|
|
86
|
-
'DBUS_SESSION_BUS_ADDRESS',
|
|
87
|
-
'GPG_AGENT_INFO',
|
|
88
|
-
'WINDOWID',
|
|
89
|
-
'DESKTOP_SESSION',
|
|
90
|
-
'SESSION_MANAGER',
|
|
91
|
-
'XAUTHORITY',
|
|
92
|
-
'WAYLAND_DISPLAY',
|
|
93
|
-
'which_declare',
|
|
94
|
-
]);
|
|
95
|
-
const systemKeyPrefixes = ['KUBERNETES_', 'npm_', 'NODE_'];
|
|
96
104
|
for (const [key, value] of Object.entries(process.env)) {
|
|
97
|
-
if (
|
|
98
|
-
if (systemKeyPrefixes.some((prefix) => key.startsWith(prefix))) continue;
|
|
105
|
+
if (isReservedEnvKey(key)) continue;
|
|
99
106
|
Underpost.env.set(key, value);
|
|
100
107
|
}
|
|
101
108
|
},
|
|
102
109
|
},
|
|
103
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @method sanitizeSecretEnvFile
|
|
113
|
+
* @description Strips shell/runtime-critical and Kubernetes-injected keys (PATH, HOME, …) from
|
|
114
|
+
* raw `.env` file content so the resulting `underpost-config` secret can be safely injected via
|
|
115
|
+
* `envFrom` without clobbering the container image's own PATH. Blank lines and comments are
|
|
116
|
+
* preserved. Uses the same {@link RESERVED_ENV_KEYS} blocklist as container-env capture.
|
|
117
|
+
* @param {string} envFileContent - Raw contents of a `.env.<env>` file.
|
|
118
|
+
* @returns {string} Filtered env-file content.
|
|
119
|
+
* @memberof UnderpostSecret
|
|
120
|
+
*/
|
|
121
|
+
sanitizeSecretEnvFile(envFileContent) {
|
|
122
|
+
return envFileContent
|
|
123
|
+
.split('\n')
|
|
124
|
+
.filter((line) => {
|
|
125
|
+
const trimmed = line.trimStart();
|
|
126
|
+
if (!trimmed || trimmed.startsWith('#')) return true;
|
|
127
|
+
const key = line.slice(0, line.indexOf('=')).trim();
|
|
128
|
+
return !key || !isReservedEnvKey(key);
|
|
129
|
+
})
|
|
130
|
+
.join('\n');
|
|
131
|
+
},
|
|
132
|
+
|
|
104
133
|
/**
|
|
105
134
|
* Removes all filesystem traces of secrets after deployment startup.
|
|
106
135
|
* Centralizes the defense-in-depth cleanup performed
|
|
136
|
+
* @param {object} options - Options for cleaning the environment.
|
|
137
|
+
* @param {Array<string>} [options.keepKeys=[]] - List of keys to keep in the environment file. If provided, only these keys will be retained.
|
|
107
138
|
* @memberof UnderpostSecret
|
|
108
139
|
*/
|
|
109
|
-
globalSecretClean() {
|
|
140
|
+
globalSecretClean(options = { keepKeys: [] }) {
|
|
141
|
+
const { keepKeys } = options;
|
|
110
142
|
loadConf('clean');
|
|
111
143
|
Underpost.repo.cleanupPrivateEngineRepo();
|
|
112
|
-
Underpost.env.clean(
|
|
144
|
+
Underpost.env.clean({
|
|
145
|
+
keepKeys: keepKeys.length > 0 ? keepKeys : ['container-status', 'start-container-status'],
|
|
146
|
+
});
|
|
113
147
|
},
|
|
114
148
|
};
|
|
115
149
|
}
|
package/src/cli/ssh.js
CHANGED
|
@@ -496,6 +496,50 @@ EOF`);
|
|
|
496
496
|
if (options.status) shellExec('service sshd status');
|
|
497
497
|
},
|
|
498
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Synchronously copies a local directory to a remote host over key-only SSH,
|
|
501
|
+
* streaming it as a tar archive (no intermediate file) and fixing ownership.
|
|
502
|
+
* Mirrors the kind-node `tar | docker cp` provisioning pattern but targets a
|
|
503
|
+
* real node via SSH, so node-local hostPath volumes can be materialized on the
|
|
504
|
+
* node where the pod will actually run.
|
|
505
|
+
*
|
|
506
|
+
* Idempotent and re-runnable: `mkdir -p` + `tar -x` overwrite in place. Throws
|
|
507
|
+
* on any SSH/tar failure so an empty-volume deploy is never produced silently.
|
|
508
|
+
*
|
|
509
|
+
* @function copyDirToNode
|
|
510
|
+
* @memberof UnderpostSSH
|
|
511
|
+
* @param {object} params
|
|
512
|
+
* @param {string} params.host - Target host/IP (key-only SSH reachable).
|
|
513
|
+
* @param {string} params.localDir - Local source directory.
|
|
514
|
+
* @param {string} params.remoteDir - Destination directory on the node.
|
|
515
|
+
* @param {number} [params.port=22] - SSH port.
|
|
516
|
+
* @param {string} [params.user='root'] - SSH user (key-only).
|
|
517
|
+
* @param {string} [params.keyPath='./engine-private/deploy/id_rsa'] - Private key path.
|
|
518
|
+
* @param {string} [params.owner='1000:1000'] - chown target on the node (empty to skip).
|
|
519
|
+
* @param {string} [params.mode='755'] - chmod mode on the node (empty to skip).
|
|
520
|
+
* @returns {void}
|
|
521
|
+
*/
|
|
522
|
+
copyDirToNode: ({
|
|
523
|
+
host,
|
|
524
|
+
localDir,
|
|
525
|
+
remoteDir,
|
|
526
|
+
port = 22,
|
|
527
|
+
user = 'root',
|
|
528
|
+
keyPath = './engine-private/deploy/id_rsa',
|
|
529
|
+
owner = '1000:1000',
|
|
530
|
+
mode = '755',
|
|
531
|
+
}) => {
|
|
532
|
+
if (!host) throw new Error('copyDirToNode requires a host');
|
|
533
|
+
if (!localDir || !fs.existsSync(localDir)) throw new Error(`copyDirToNode: local dir not found: ${localDir}`);
|
|
534
|
+
if (!remoteDir) throw new Error('copyDirToNode requires a remoteDir');
|
|
535
|
+
shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
|
|
536
|
+
const sshOpts = `-i ${keyPath} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
|
|
537
|
+
shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`);
|
|
538
|
+
shellExec(`tar -C ${localDir} -c . | ssh ${sshOpts} ${user}@${host} 'tar -C ${remoteDir} -x'`);
|
|
539
|
+
const fixups = `${owner ? `chown -R ${owner} ${remoteDir}; ` : ''}${mode ? `chmod -R ${mode} ${remoteDir}` : ''}`.trim();
|
|
540
|
+
if (fixups) shellExec(`ssh ${sshOpts} ${user}@${host} '${fixups}'`);
|
|
541
|
+
},
|
|
542
|
+
|
|
499
543
|
/**
|
|
500
544
|
* Generic SSH remote command runner that SSH execution logic.
|
|
501
545
|
* Executes arbitrary shell commands on a remote server via SSH with proper credential handling.
|
|
@@ -545,6 +589,196 @@ EOF
|
|
|
545
589
|
return shellExec(sshScript, { stdout: true });
|
|
546
590
|
},
|
|
547
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Waits until a TCP SSH port becomes reachable on a host.
|
|
594
|
+
* @async
|
|
595
|
+
* @function waitForSshPort
|
|
596
|
+
* @memberof UnderpostSSH
|
|
597
|
+
* @param {object} params
|
|
598
|
+
* @param {string} params.host - Target host/IP.
|
|
599
|
+
* @param {number} [params.port=22] - SSH port.
|
|
600
|
+
* @param {number} [params.timeoutMs=600000] - Maximum wait window.
|
|
601
|
+
* @param {number} [params.intervalMs=3000] - Poll interval.
|
|
602
|
+
* @returns {Promise<boolean>} True once the port accepts connections, false on timeout.
|
|
603
|
+
*/
|
|
604
|
+
waitForSshPort: async ({ host, port = 22, timeoutMs = 10 * 60 * 1000, intervalMs = 3000 }) => {
|
|
605
|
+
const deadline = Date.now() + timeoutMs;
|
|
606
|
+
while (Date.now() < deadline) {
|
|
607
|
+
const probe = shellExec(
|
|
608
|
+
`timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
|
|
609
|
+
{ silent: true, stdout: true, silentOnError: true, disableLog: true },
|
|
610
|
+
);
|
|
611
|
+
if (`${probe}`.trim() === 'open') return true;
|
|
612
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
613
|
+
}
|
|
614
|
+
logger.warn(`SSH port ${host}:${port} not reachable within timeout`);
|
|
615
|
+
return false;
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Waits until a host's SSH port stops accepting connections (e.g. while it
|
|
620
|
+
* reboots). Used to detect a reboot edge before waiting for the port to come
|
|
621
|
+
* back up, so callers don't latch onto the pre-reboot (ephemeral) sshd.
|
|
622
|
+
* @async
|
|
623
|
+
* @function waitForSshPortClosed
|
|
624
|
+
* @memberof UnderpostSSH
|
|
625
|
+
* @param {object} params
|
|
626
|
+
* @param {string} params.host - Target host/IP.
|
|
627
|
+
* @param {number} [params.port=22] - SSH port.
|
|
628
|
+
* @param {number} [params.timeoutMs=180000] - Maximum wait window.
|
|
629
|
+
* @param {number} [params.intervalMs=3000] - Poll interval.
|
|
630
|
+
* @returns {Promise<boolean>} True once the port is closed, false on timeout.
|
|
631
|
+
*/
|
|
632
|
+
waitForSshPortClosed: async ({ host, port = 22, timeoutMs = 3 * 60 * 1000, intervalMs = 3000 }) => {
|
|
633
|
+
const deadline = Date.now() + timeoutMs;
|
|
634
|
+
while (Date.now() < deadline) {
|
|
635
|
+
const probe = shellExec(
|
|
636
|
+
`timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
|
|
637
|
+
{ silent: true, stdout: true, silentOnError: true, disableLog: true },
|
|
638
|
+
);
|
|
639
|
+
if (`${probe}`.trim() === 'closed') return true;
|
|
640
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Orchestrates a non-interactive, key-only SSH session against a freshly
|
|
647
|
+
* provisioned host: waits for the port, attempts key-based auth, runs a
|
|
648
|
+
* remote command batch, and returns a structured result. Used by the
|
|
649
|
+
* commissioning flow once the ephemeral runtime reports SSH readiness.
|
|
650
|
+
* @async
|
|
651
|
+
* @function sshExecBatch
|
|
652
|
+
* @memberof UnderpostSSH
|
|
653
|
+
* @param {object} params
|
|
654
|
+
* @param {string} params.host - Target host/IP.
|
|
655
|
+
* @param {string} params.command - Remote command batch to execute.
|
|
656
|
+
* @param {number} [params.port=22] - SSH port.
|
|
657
|
+
* @param {string} [params.user='root'] - SSH user (key-only).
|
|
658
|
+
* @param {string} [params.keyPath] - Private key path (defaults to engine deploy key).
|
|
659
|
+
* @param {number} [params.connectTimeoutSec=15] - Per-attempt SSH connect timeout.
|
|
660
|
+
* @param {number} [params.retries=3] - Auth/exec retry attempts.
|
|
661
|
+
* @param {number} [params.retryDelayMs=5000] - Base backoff between retries.
|
|
662
|
+
* @param {number} [params.waitForPortMs=0] - When > 0, wait for the port first.
|
|
663
|
+
* @returns {Promise<{ok: boolean, code: number, stdout: string, stderr: string, attempts: number}>}
|
|
664
|
+
*/
|
|
665
|
+
sshExecBatch: async ({
|
|
666
|
+
host,
|
|
667
|
+
command,
|
|
668
|
+
port = 22,
|
|
669
|
+
user = 'root',
|
|
670
|
+
keyPath = './engine-private/deploy/id_rsa',
|
|
671
|
+
connectTimeoutSec = 15,
|
|
672
|
+
retries = 3,
|
|
673
|
+
retryDelayMs = 5000,
|
|
674
|
+
waitForPortMs = 0,
|
|
675
|
+
}) => {
|
|
676
|
+
if (!host) throw new Error('sshExecBatch requires a host');
|
|
677
|
+
if (!command) throw new Error('sshExecBatch requires a command');
|
|
678
|
+
|
|
679
|
+
if (waitForPortMs > 0) {
|
|
680
|
+
const reachable = await Underpost.ssh.waitForSshPort({ host, port, timeoutMs: waitForPortMs });
|
|
681
|
+
if (!reachable) return { ok: false, code: 255, stdout: '', stderr: 'ssh port unreachable', attempts: 0 };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
shellExec(`chmod 600 ${keyPath}`, { silent: true, silentOnError: true, disableLog: true });
|
|
685
|
+
|
|
686
|
+
const sshOpts = [
|
|
687
|
+
`-i ${keyPath}`,
|
|
688
|
+
`-o BatchMode=yes`,
|
|
689
|
+
`-o PreferredAuthentications=publickey`,
|
|
690
|
+
`-o PubkeyAuthentication=yes`,
|
|
691
|
+
`-o PasswordAuthentication=no`,
|
|
692
|
+
`-o StrictHostKeyChecking=no`,
|
|
693
|
+
`-o UserKnownHostsFile=/dev/null`,
|
|
694
|
+
`-o ConnectTimeout=${connectTimeoutSec}`,
|
|
695
|
+
// Tolerate a freshly-booted node whose network briefly flaps (e.g. while
|
|
696
|
+
// NetworkManager applies a static profile): retry the TCP connect and
|
|
697
|
+
// keep the session alive across short stalls.
|
|
698
|
+
`-o ConnectionAttempts=3`,
|
|
699
|
+
`-o ServerAliveInterval=10`,
|
|
700
|
+
`-o ServerAliveCountMax=6`,
|
|
701
|
+
`-p ${port}`,
|
|
702
|
+
].join(' ');
|
|
703
|
+
|
|
704
|
+
let last = { ok: false, code: 255, stdout: '', stderr: '', attempts: 0 };
|
|
705
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
706
|
+
const result = shellExec(`ssh ${sshOpts} ${user}@${host} bash -s <<'UNDERPOST_SSH_BATCH_EOF'\n${command}\nUNDERPOST_SSH_BATCH_EOF`, {
|
|
707
|
+
stdout: false,
|
|
708
|
+
silentOnError: true,
|
|
709
|
+
disableLog: true,
|
|
710
|
+
});
|
|
711
|
+
last = {
|
|
712
|
+
ok: result.code === 0,
|
|
713
|
+
code: result.code,
|
|
714
|
+
stdout: result.stdout || '',
|
|
715
|
+
stderr: result.stderr || '',
|
|
716
|
+
attempts: attempt,
|
|
717
|
+
};
|
|
718
|
+
if (last.ok) {
|
|
719
|
+
logger.info(`sshExecBatch succeeded on ${user}@${host}:${port} (attempt ${attempt})`);
|
|
720
|
+
return last;
|
|
721
|
+
}
|
|
722
|
+
logger.warn(`sshExecBatch attempt ${attempt}/${retries} failed on ${user}@${host}:${port}`, {
|
|
723
|
+
code: last.code,
|
|
724
|
+
stderr: last.stderr.slice(-400),
|
|
725
|
+
});
|
|
726
|
+
if (attempt < retries) await new Promise((r) => setTimeout(r, retryDelayMs * attempt));
|
|
727
|
+
}
|
|
728
|
+
return last;
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Transfers a local script to a remote host and runs it over key-only SSH.
|
|
733
|
+
* The script is base64-encoded so no shell-quoting/escaping is needed, then
|
|
734
|
+
* decoded, made executable, and executed with the given arguments. Reuses
|
|
735
|
+
* sshExecBatch for the actual transport, retries, and structured result.
|
|
736
|
+
* @async
|
|
737
|
+
* @function sshRunScript
|
|
738
|
+
* @memberof UnderpostSSH
|
|
739
|
+
* @param {object} params
|
|
740
|
+
* @param {string} params.host - Target host/IP.
|
|
741
|
+
* @param {string} params.scriptPath - Local path to the script to run.
|
|
742
|
+
* @param {string} [params.args=''] - Arguments appended to the remote invocation.
|
|
743
|
+
* @param {object} [params.env={}] - Environment variables exported for the remote run (e.g. secrets). Passed inline to the command, never echoed.
|
|
744
|
+
* @param {string} [params.remotePath='/tmp/underpost-remote-script.sh'] - Remote path to write the script.
|
|
745
|
+
* @param {number} [params.port=22] - SSH port.
|
|
746
|
+
* @param {string} [params.user='root'] - SSH user (key-only).
|
|
747
|
+
* @param {string} [params.keyPath] - Private key path (defaults to engine deploy key).
|
|
748
|
+
* @param {number} [params.retries=3] - Retry attempts.
|
|
749
|
+
* @param {number} [params.waitForPortMs=0] - When > 0, wait for the port first.
|
|
750
|
+
* @returns {Promise<{ok: boolean, code: number, stdout: string, stderr: string, attempts: number}>}
|
|
751
|
+
*/
|
|
752
|
+
sshRunScript: async ({
|
|
753
|
+
host,
|
|
754
|
+
scriptPath,
|
|
755
|
+
args = '',
|
|
756
|
+
env = {},
|
|
757
|
+
remotePath = '/tmp/underpost-remote-script.sh',
|
|
758
|
+
port = 22,
|
|
759
|
+
user = 'root',
|
|
760
|
+
keyPath = './engine-private/deploy/id_rsa',
|
|
761
|
+
retries = 3,
|
|
762
|
+
waitForPortMs = 0,
|
|
763
|
+
}) => {
|
|
764
|
+
if (!fs.existsSync(scriptPath)) throw new Error(`sshRunScript: script not found: ${scriptPath}`);
|
|
765
|
+
const b64 = Buffer.from(fs.readFileSync(scriptPath, 'utf8'), 'utf8').toString('base64');
|
|
766
|
+
// Inline env assignments (single-quote escaped) so secrets are exported for
|
|
767
|
+
// the remote run without appearing as logged CLI args.
|
|
768
|
+
const sq = (v) => `'${String(v).replace(/'/g, "'\\''")}'`;
|
|
769
|
+
const envPrefix = Object.entries(env)
|
|
770
|
+
.filter(([, v]) => v !== undefined && v !== null && `${v}` !== '')
|
|
771
|
+
.map(([k, v]) => `${k}=${sq(v)}`)
|
|
772
|
+
.join(' ');
|
|
773
|
+
const command = [
|
|
774
|
+
'set -e',
|
|
775
|
+
`echo '${b64}' | base64 -d > ${remotePath}`,
|
|
776
|
+
`chmod +x ${remotePath}`,
|
|
777
|
+
`${envPrefix ? `${envPrefix} ` : ''}bash ${remotePath} ${args}`,
|
|
778
|
+
].join('\n');
|
|
779
|
+
return Underpost.ssh.sshExecBatch({ host, port, user, keyPath, retries, waitForPortMs, command });
|
|
780
|
+
},
|
|
781
|
+
|
|
548
782
|
/**
|
|
549
783
|
* Loads saved SSH credentials from config and sets them in the UnderpostRootEnv API.
|
|
550
784
|
* @async
|
package/src/cli/static.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import express from 'express';
|
|
9
|
-
import { ssrFactory } from '../
|
|
9
|
+
import { ssrFactory } from '../client-builder/ssr.js';
|
|
10
10
|
import { shellExec } from '../server/process.js';
|
|
11
11
|
import Underpost from '../index.js';
|
|
12
|
-
import { JSONweb } from '../
|
|
12
|
+
import { JSONweb } from '../client-builder/client-formatted.js';
|
|
13
13
|
import { loggerFactory, loggerMiddleware } from '../server/logger.js';
|
|
14
14
|
const logger = loggerFactory(import.meta);
|
|
15
15
|
/**
|