underpost 2.8.881 → 2.8.883
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/.github/workflows/release.cd.yml +1 -2
- package/README.md +50 -36
- package/bin/db.js +1 -4
- package/cli.md +86 -86
- package/conf.js +1 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +6 -6
- package/manifests/maas/device-scan.sh +1 -1
- package/package.json +1 -1
- package/src/api/document/document.service.js +9 -1
- package/src/cli/repository.js +2 -0
- package/src/client/components/core/Auth.js +258 -89
- package/src/client/components/core/BtnIcon.js +10 -1
- package/src/client/components/core/CssCore.js +36 -27
- package/src/client/components/core/Docs.js +188 -85
- package/src/client/components/core/LoadingAnimation.js +5 -10
- package/src/client/components/core/Modal.js +262 -120
- package/src/client/components/core/ObjectLayerEngine.js +154 -158
- package/src/client/components/core/Panel.js +2 -0
- package/src/client/components/core/PanelForm.js +94 -60
- package/src/client/components/core/Router.js +15 -15
- package/src/client/components/core/ToolTip.js +83 -19
- package/src/client/components/core/Translate.js +1 -1
- package/src/client/components/core/VanillaJs.js +4 -3
- package/src/client/components/core/windowGetDimensions.js +202 -0
- package/src/client/components/default/MenuDefault.js +11 -0
- package/src/client/ssr/Render.js +1 -1
- package/src/index.js +1 -1
- package/src/runtime/lampp/Lampp.js +253 -128
- package/src/server/auth.js +68 -17
- package/src/server/crypto.js +195 -76
- package/src/server/peer.js +47 -5
- package/src/server/process.js +85 -1
- package/src/server/runtime.js +13 -32
- package/test/crypto.test.js +117 -0
- package/src/runtime/xampp/Xampp.js +0 -83
package/src/server/crypto.js
CHANGED
|
@@ -1,91 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module for managing crypto operations
|
|
3
|
+
* @module src/server/crypto.js
|
|
4
|
+
* @namespace Crypto
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import crypto from 'crypto';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
|
|
9
|
+
/* ----------------------------- SymmetricCrypto ----------------------------- */
|
|
10
|
+
|
|
11
|
+
class SymmetricCrypto {
|
|
12
|
+
#encryptionKey;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {Buffer | string} [options.encryptionKey] - 32-byte key as Buffer or hex string. If not provided, a new random key is generated.
|
|
17
|
+
*/
|
|
18
|
+
/** @memberof Crypto */
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
const { encryptionKey } = options;
|
|
21
|
+
|
|
22
|
+
if (encryptionKey) {
|
|
23
|
+
this.#encryptionKey = typeof encryptionKey === 'string' ? Buffer.from(encryptionKey, 'hex') : encryptionKey;
|
|
24
|
+
} else {
|
|
25
|
+
this.#encryptionKey = crypto.randomBytes(32);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!Buffer.isBuffer(this.#encryptionKey) || this.#encryptionKey.length !== 32) {
|
|
29
|
+
throw new Error('Encryption key must be a 32-byte Buffer or 64-length hex string.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Provide a compatibility IV property expected by some test suites / legacy code.
|
|
33
|
+
// This IV is not reused for each encryption operation (encryptData will generate its own IV).
|
|
34
|
+
// It exists so tests that expect an ivHex on the instance (16 bytes) continue to work.
|
|
35
|
+
this.ivHex = crypto.randomBytes(16).toString('hex'); // 16 bytes -> 32 hex chars
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returns encryption key as hex. */
|
|
39
|
+
/** @memberof Crypto */
|
|
40
|
+
get encryptionKeyHex() {
|
|
41
|
+
return this.#encryptionKey.toString('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Encrypts plaintext using AES-256-GCM and returns `iv_hex:ciphertext_hex:authTag_hex`.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} [plaintext='']
|
|
48
|
+
* @returns {string}
|
|
49
|
+
* @memberof Crypto
|
|
50
|
+
*/
|
|
51
|
+
encryptData(plaintext = '') {
|
|
52
|
+
// GCM recommended IV size is 12 bytes
|
|
53
|
+
const iv = crypto.randomBytes(12);
|
|
54
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', this.#encryptionKey, iv);
|
|
55
|
+
|
|
56
|
+
const encryptedPart = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
57
|
+
const authTag = cipher.getAuthTag();
|
|
58
|
+
|
|
59
|
+
return `${iv.toString('hex')}:${encryptedPart.toString('hex')}:${authTag.toString('hex')}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decrypts data. Supports two formats:
|
|
64
|
+
* - AES-256-GCM: `iv_hex:ciphertext_hex:authTag_hex` (preferred)
|
|
65
|
+
* - Legacy AES-256-CBC: `iv_hex:ciphertext_hex` (fallback for backward compatibility)
|
|
66
|
+
*
|
|
67
|
+
* @param {string} [ciphertext='']
|
|
68
|
+
* @returns {string} plaintext
|
|
69
|
+
* @throws {Error} Generic error on failure (to avoid leaking details).
|
|
70
|
+
* @memberof Crypto
|
|
71
|
+
*/
|
|
72
|
+
decryptData(ciphertext = '') {
|
|
73
|
+
try {
|
|
74
|
+
const parts = ciphertext.split(':');
|
|
75
|
+
|
|
76
|
+
if (parts.length === 3) {
|
|
77
|
+
// AES-256-GCM
|
|
78
|
+
const [ivHex, encryptedHex, tagHex] = parts;
|
|
79
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
80
|
+
const encrypted = Buffer.from(encryptedHex, 'hex');
|
|
81
|
+
const authTag = Buffer.from(tagHex, 'hex');
|
|
82
|
+
|
|
83
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', this.#encryptionKey, iv);
|
|
84
|
+
decipher.setAuthTag(authTag);
|
|
85
|
+
|
|
86
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
87
|
+
return decrypted.toString('utf8');
|
|
17
88
|
}
|
|
18
89
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const [ivHex,
|
|
22
|
-
const
|
|
23
|
-
const
|
|
90
|
+
if (parts.length === 2) {
|
|
91
|
+
// Legacy: AES-256-CBC (no authentication). Provided for compatibility only.
|
|
92
|
+
const [ivHex, encryptedHex] = parts;
|
|
93
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
94
|
+
const encrypted = encryptedHex;
|
|
95
|
+
|
|
96
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', this.#encryptionKey, iv);
|
|
24
97
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
25
98
|
decrypted += decipher.final('utf8');
|
|
26
99
|
return decrypted;
|
|
27
100
|
}
|
|
28
101
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
asymmetric: {
|
|
38
|
-
instance: function (
|
|
39
|
-
options = {
|
|
40
|
-
publicKey: '', // fs.readFileSync('./key.pem', 'utf8')
|
|
41
|
-
privateKey: '', // fs.readFileSync('./key.pem', 'utf8')
|
|
42
|
-
},
|
|
43
|
-
) {
|
|
44
|
-
// Generate a new key pair
|
|
45
|
-
const { privateKey, publicKey } = options
|
|
46
|
-
? options
|
|
47
|
-
: crypto.generateKeyPairSync('rsa', {
|
|
48
|
-
modulusLength: 2048, // Key size in bits
|
|
49
|
-
publicKeyEncoding: {
|
|
50
|
-
type: 'spki',
|
|
51
|
-
format: 'pem',
|
|
52
|
-
},
|
|
53
|
-
privateKeyEncoding: {
|
|
54
|
-
type: 'pkcs8',
|
|
55
|
-
format: 'pem',
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Function to encrypt data
|
|
60
|
-
function encryptData(plaintext) {
|
|
61
|
-
const buffer = Buffer.from(plaintext, 'utf8');
|
|
62
|
-
const encrypted = crypto.publicEncrypt(publicKey, buffer);
|
|
63
|
-
return encrypted.toString('hex');
|
|
64
|
-
}
|
|
102
|
+
throw new Error('Invalid ciphertext format.');
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Do not leak internal error details (stack, key material, etc.).
|
|
105
|
+
// Optional: instrument monitoring/logging but avoid logging sensitive inputs.
|
|
106
|
+
throw new Error('Decryption failed. Check key, IV, or ciphertext integrity.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
65
110
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
111
|
+
/* ---------------------------- AsymmetricCrypto ---------------------------- */
|
|
112
|
+
|
|
113
|
+
class AsymmetricCrypto {
|
|
114
|
+
#publicKey;
|
|
115
|
+
#privateKey;
|
|
116
|
+
#modulusLength;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {object} [options]
|
|
120
|
+
* @param {string|Buffer} [options.publicKey] - PEM-formatted public key
|
|
121
|
+
* @param {string|Buffer} [options.privateKey] - PEM-formatted private key
|
|
122
|
+
* @param {number} [options.modulusLength=2048] - If keys are not provided, generates a new key pair of this size (bits). Consider 3072 for long-lived keys.
|
|
123
|
+
*/
|
|
124
|
+
/** @memberof Crypto */
|
|
125
|
+
constructor(options = {}) {
|
|
126
|
+
const { publicKey, privateKey } = options;
|
|
127
|
+
this.#modulusLength = options.modulusLength || 2048;
|
|
128
|
+
|
|
129
|
+
if (!publicKey || !privateKey) {
|
|
130
|
+
// Generate an in-memory key pair. No file I/O; keys remain in process memory only.
|
|
131
|
+
const { publicKey: pub, privateKey: priv } = crypto.generateKeyPairSync('rsa', {
|
|
132
|
+
modulusLength: this.#modulusLength,
|
|
133
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
134
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
135
|
+
});
|
|
136
|
+
this.#publicKey = pub;
|
|
137
|
+
this.#privateKey = priv;
|
|
138
|
+
} else {
|
|
139
|
+
// Accept provided keys (string or Buffer)
|
|
140
|
+
this.#publicKey = typeof publicKey === 'string' || Buffer.isBuffer(publicKey) ? publicKey : String(publicKey);
|
|
141
|
+
this.#privateKey =
|
|
142
|
+
typeof privateKey === 'string' || Buffer.isBuffer(privateKey) ? privateKey : String(privateKey);
|
|
143
|
+
|
|
144
|
+
// Basic validation: ensure PEM headers exist. This is intentionally lightweight.
|
|
145
|
+
const pubStr = String(this.#publicKey);
|
|
146
|
+
const privStr = String(this.#privateKey);
|
|
147
|
+
if (!pubStr.includes('BEGIN PUBLIC KEY') || !privStr.includes('BEGIN')) {
|
|
148
|
+
throw new Error('Provided keys do not appear to be valid PEM-formatted keys.');
|
|
71
149
|
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
72
152
|
|
|
73
|
-
|
|
74
|
-
|
|
153
|
+
/** @memberof Crypto */
|
|
154
|
+
get publicKey() {
|
|
155
|
+
return this.#publicKey;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @memberof Crypto */
|
|
159
|
+
get privateKey() {
|
|
160
|
+
return this.#privateKey;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Encrypts plaintext using RSA-OAEP with SHA-256. Returns hex-encoded ciphertext.
|
|
165
|
+
* Note: RSA encryption is intended for small payloads. For larger data use hybrid encryption (encrypt a symmetric key and then use AES-GCM).
|
|
166
|
+
* @param {string} plaintext
|
|
167
|
+
* @returns {string} hex ciphertext
|
|
168
|
+
* @memberof Crypto
|
|
169
|
+
*/
|
|
170
|
+
encryptData(plaintext) {
|
|
171
|
+
const buffer = Buffer.from(plaintext, 'utf8');
|
|
172
|
+
const encrypted = crypto.publicEncrypt(
|
|
173
|
+
{
|
|
174
|
+
key: this.#publicKey,
|
|
175
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
176
|
+
oaepHash: 'sha256',
|
|
177
|
+
},
|
|
178
|
+
buffer,
|
|
179
|
+
);
|
|
75
180
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
publicKey: fs.readFileSync('./private.pem', 'utf8'),
|
|
79
|
-
encryptData,
|
|
80
|
-
decryptData,
|
|
81
|
-
};
|
|
181
|
+
return encrypted.toString('hex');
|
|
182
|
+
}
|
|
82
183
|
|
|
83
|
-
|
|
84
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Decrypts RSA-OAEP hex ciphertext and returns utf8 plaintext.
|
|
186
|
+
* @param {string} ciphertextHex
|
|
187
|
+
* @returns {string}
|
|
188
|
+
* @memberof Crypto
|
|
189
|
+
*/
|
|
190
|
+
decryptData(ciphertextHex) {
|
|
191
|
+
try {
|
|
192
|
+
const buffer = Buffer.from(ciphertextHex, 'hex');
|
|
193
|
+
const decrypted = crypto.privateDecrypt(
|
|
194
|
+
{
|
|
195
|
+
key: this.#privateKey,
|
|
196
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
197
|
+
oaepHash: 'sha256',
|
|
198
|
+
},
|
|
199
|
+
buffer,
|
|
200
|
+
);
|
|
85
201
|
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
202
|
+
return decrypted.toString('utf8');
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Avoid leaking details about keys or ciphertext
|
|
205
|
+
throw new Error('Decryption failed. Check private key or ciphertext integrity.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
90
209
|
|
|
91
|
-
export {
|
|
210
|
+
export { SymmetricCrypto, AsymmetricCrypto };
|
package/src/server/peer.js
CHANGED
|
@@ -1,30 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module for peerjs server management.
|
|
3
|
+
* Initializes and configures the PeerJS server instance, typically running
|
|
4
|
+
* alongside a main Node.js application.
|
|
5
|
+
*
|
|
6
|
+
* @module src/server/peer.js
|
|
7
|
+
* @namespace Peer
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { PeerServer } from 'peer';
|
|
2
11
|
import dotenv from 'dotenv';
|
|
3
12
|
import { loggerFactory } from './logger.js';
|
|
4
|
-
import fs from 'fs-extra';
|
|
5
13
|
import UnderpostStartUp from './start.js';
|
|
6
14
|
|
|
7
15
|
dotenv.config();
|
|
8
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Logger instance for this module, utilizing the framework's factory.
|
|
19
|
+
* @type {function(*): void}
|
|
20
|
+
* @memberof Peer
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
9
23
|
const logger = loggerFactory(import.meta);
|
|
10
24
|
|
|
25
|
+
// Documentation references:
|
|
11
26
|
// https://github.com/peers/peerjs
|
|
12
27
|
// https://github.com/peers/peerjs-server
|
|
13
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates and starts a configured PeerJS server instance.
|
|
31
|
+
*
|
|
32
|
+
* This function handles port configuration, CORS origins, and paths, then uses
|
|
33
|
+
* a listener factory to start the server.
|
|
34
|
+
*
|
|
35
|
+
* @async
|
|
36
|
+
* @function createPeerServer
|
|
37
|
+
* @memberof Peer
|
|
38
|
+
* @param {object} config - Configuration object for the PeerJS server setup.
|
|
39
|
+
* @param {number} config.port - The primary port on which the PeerJS server will listen.
|
|
40
|
+
* @param {number} [config.devPort] - Optional development port. If provided and in 'development' NODE_ENV, 'http://localhost:${devPort}' is added to allowed origins.
|
|
41
|
+
* @param {string[]} config.origins - An array of allowed domain origins for Cross-Origin Resource Sharing (CORS).
|
|
42
|
+
* @param {string} config.host - The host address the server is bound to (used internally for configuration).
|
|
43
|
+
* @param {string} config.path - The base path for the API. The PeerJS path ('/peer') will be appended to this.
|
|
44
|
+
* @returns {Promise<object>} A promise that resolves to an object containing the final configuration and the server instance.
|
|
45
|
+
* @returns {import('peer').IConfig} return.options - The final options object used to create the PeerServer.
|
|
46
|
+
* @returns {import('peer').Server} return.peerServer - The created and listening PeerServer instance (wrapped by the listening server factory).
|
|
47
|
+
* @returns {object} return.meta - The module's import meta object (`import.meta`).
|
|
48
|
+
*/
|
|
14
49
|
const createPeerServer = async ({ port, devPort, origins, host, path }) => {
|
|
15
|
-
if (process.env.NODE_ENV === 'development' && devPort)
|
|
50
|
+
if (process.env.NODE_ENV === 'development' && devPort) {
|
|
51
|
+
logger.warn(`Adding development origin: http://localhost:${devPort}`);
|
|
52
|
+
origins.push(`http://localhost:${devPort}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
/** @type {import('peer').IConfig} */
|
|
17
56
|
const options = {
|
|
18
57
|
port,
|
|
58
|
+
// Ensure the path is correctly formatted, handling the root path case
|
|
19
59
|
path: `${path === '/' ? '' : path}/peer`,
|
|
20
60
|
corsOptions: {
|
|
21
61
|
origin: origins,
|
|
22
62
|
},
|
|
23
63
|
proxied: true,
|
|
24
|
-
// key: fs.readFileSync(''),
|
|
25
|
-
// cert: fs.readFileSync(''),
|
|
26
|
-
// ca: fs.readFileSync(''),
|
|
64
|
+
// key: fs.readFileSync(''), // Example for HTTPS/SSL
|
|
65
|
+
// cert: fs.readFileSync(''), // Example for HTTPS/SSL
|
|
66
|
+
// ca: fs.readFileSync(''), // Example for HTTPS/SSL
|
|
27
67
|
};
|
|
68
|
+
|
|
69
|
+
// Use the framework's factory to listen on the server, ensuring graceful startup/shutdown
|
|
28
70
|
const peerServer = UnderpostStartUp.API.listenServerFactory(() => PeerServer(options));
|
|
29
71
|
|
|
30
72
|
return { options, peerServer, meta: import.meta };
|
package/src/server/process.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module for process and shell command management.
|
|
3
|
+
* Provides utilities for executing shell commands, managing signals, and handling environment details.
|
|
4
|
+
* @module src/server/process.js
|
|
5
|
+
* @namespace Process
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
// https://nodejs.org/api/process
|
|
2
9
|
|
|
3
10
|
import shell from 'shelljs';
|
|
4
11
|
import dotenv from 'dotenv';
|
|
5
|
-
import fs from 'fs-extra';
|
|
6
12
|
import { loggerFactory } from './logger.js';
|
|
7
13
|
import clipboard from 'clipboardy';
|
|
8
14
|
import UnderpostRootEnv from '../cli/env.js';
|
|
@@ -11,9 +17,23 @@ dotenv.config();
|
|
|
11
17
|
|
|
12
18
|
const logger = loggerFactory(import.meta);
|
|
13
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Gets the current working directory, replacing backslashes with forward slashes for consistency.
|
|
22
|
+
* @memberof Process
|
|
23
|
+
* @returns {string} The root directory path.
|
|
24
|
+
*/
|
|
14
25
|
const getRootDirectory = () => process.cwd().replace(/\\/g, '/');
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Controls and manages process-level events and signals.
|
|
29
|
+
* @namespace ProcessController
|
|
30
|
+
*/
|
|
16
31
|
const ProcessController = {
|
|
32
|
+
/**
|
|
33
|
+
* List of signals to listen for for graceful shutdown/handling.
|
|
34
|
+
* @memberof ProcessController
|
|
35
|
+
* @type {string[]}
|
|
36
|
+
*/
|
|
17
37
|
SIG: [
|
|
18
38
|
'SIGPIPE',
|
|
19
39
|
'SIGHUP',
|
|
@@ -28,6 +48,13 @@ const ProcessController = {
|
|
|
28
48
|
'SIGSEGV',
|
|
29
49
|
'SIGILL',
|
|
30
50
|
],
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets up listeners for various process signals defined in {@link ProcessController.SIG}.
|
|
54
|
+
* Handles graceful exit on 'SIGINT' (Ctrl+C).
|
|
55
|
+
* @memberof ProcessController
|
|
56
|
+
* @returns {Array<process.Process>} An array of process listener handles.
|
|
57
|
+
*/
|
|
31
58
|
onSigListen: function () {
|
|
32
59
|
return this.SIG.map((sig) =>
|
|
33
60
|
process.on(sig, (...args) => {
|
|
@@ -42,6 +69,14 @@ const ProcessController = {
|
|
|
42
69
|
}),
|
|
43
70
|
);
|
|
44
71
|
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initializes the ProcessController.
|
|
75
|
+
* Sets up signal listeners, registers a listener for the 'exit' event, and cleans up temporary deployment environment variables.
|
|
76
|
+
* @memberof ProcessController
|
|
77
|
+
* @param {Object} logger - The logger instance to use for internal logging.
|
|
78
|
+
* @returns {void}
|
|
79
|
+
*/
|
|
45
80
|
init: function (logger) {
|
|
46
81
|
this.logger = logger;
|
|
47
82
|
process.on('exit', (...args) => {
|
|
@@ -52,33 +87,82 @@ const ProcessController = {
|
|
|
52
87
|
},
|
|
53
88
|
};
|
|
54
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Executes a shell command using shelljs.
|
|
92
|
+
* @memberof Process
|
|
93
|
+
* @param {string} cmd - The command string to execute.
|
|
94
|
+
* @param {Object} [options] - Options for execution.
|
|
95
|
+
* @param {boolean} [options.silent=false] - Suppress output from shell commands.
|
|
96
|
+
* @param {boolean} [options.async=false] - Run command asynchronously.
|
|
97
|
+
* @param {boolean} [options.stdout=false] - Return stdout content (string) instead of shelljs result object.
|
|
98
|
+
* @param {boolean} [options.disableLog=false] - Prevent logging of the command.
|
|
99
|
+
* @returns {string|shelljs.ShellString} The result of the shell command (string if `stdout: true`, otherwise a ShellString object).
|
|
100
|
+
*/
|
|
55
101
|
const shellExec = (cmd, options = { silent: false, async: false, stdout: false, disableLog: false }) => {
|
|
56
102
|
if (!options.disableLog) logger.info(`cmd`, cmd);
|
|
57
103
|
return options.stdout ? shell.exec(cmd, options).stdout : shell.exec(cmd, options);
|
|
58
104
|
};
|
|
59
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Changes the current working directory using shelljs.
|
|
108
|
+
* @memberof Process
|
|
109
|
+
* @param {string} cd - The path to change the directory to.
|
|
110
|
+
* @param {Object} [options] - Options for the CD operation.
|
|
111
|
+
* @param {boolean} [options.disableLog=false] - Prevent logging of the CD command.
|
|
112
|
+
* @returns {shelljs.ShellString} The result of the shelljs cd command.
|
|
113
|
+
*/
|
|
60
114
|
const shellCd = (cd, options = { disableLog: false }) => {
|
|
61
115
|
if (!options.disableLog) logger.info(`cd`, cd);
|
|
62
116
|
return shell.cd(cd);
|
|
63
117
|
};
|
|
64
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Opens a new GNOME terminal and executes a command.
|
|
121
|
+
* Note: This function is environment-specific (GNOME/Linux).
|
|
122
|
+
* @memberof Process
|
|
123
|
+
* @param {string} cmd - The command to execute in the new terminal.
|
|
124
|
+
* @param {Object} [options] - Options for the terminal opening.
|
|
125
|
+
* @param {boolean} [options.single=false] - If true, execute as a single session process using `setsid`.
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
65
128
|
const openTerminal = (cmd, options = { single: false }) => {
|
|
66
129
|
if (options.single === true) {
|
|
130
|
+
// Run as a single session process
|
|
67
131
|
shellExec(`setsid gnome-terminal -- bash -ic "${cmd}; exec bash" >/dev/null 2>&1 &`);
|
|
68
132
|
return;
|
|
69
133
|
}
|
|
134
|
+
// Run asynchronously and disown
|
|
70
135
|
shellExec(`gnome-terminal -- bash -c "${cmd}; exec bash" & disown`, {
|
|
71
136
|
async: true,
|
|
72
137
|
stdout: true,
|
|
73
138
|
});
|
|
74
139
|
};
|
|
75
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Wraps a command to run it as a daemon process in a shell (keeping the process alive/terminal open).
|
|
143
|
+
* @memberof Process
|
|
144
|
+
* @param {string} cmd - The command to daemonize.
|
|
145
|
+
* @returns {string} The shell command string for the daemon process.
|
|
146
|
+
*/
|
|
76
147
|
const daemonProcess = (cmd) => `exec bash -c '${cmd}; exec tail -f /dev/null'`;
|
|
77
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Retrieves the process ID (PID) of the most recently created gnome-terminal instance.
|
|
151
|
+
* Note: This function is environment-specific (GNOME/Linux) and uses `pgrep -n`.
|
|
152
|
+
* @memberof Process
|
|
153
|
+
* @returns {number} The PID of the last gnome-terminal process.
|
|
154
|
+
*/
|
|
78
155
|
// list all terminals: pgrep gnome-terminal
|
|
79
156
|
// list last terminal: pgrep -n gnome-terminal
|
|
80
157
|
const getTerminalPid = () => JSON.parse(shellExec(`pgrep -n gnome-terminal`, { stdout: true, silent: true }));
|
|
81
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Copies text content to the system clipboard using clipboardy.
|
|
161
|
+
* Logs the copied content for confirmation.
|
|
162
|
+
* @memberof Process
|
|
163
|
+
* @param {string} [data='🦄'] - The data to copy. Defaults to '🦄'.
|
|
164
|
+
* @returns {void}
|
|
165
|
+
*/
|
|
82
166
|
function pbcopy(data) {
|
|
83
167
|
clipboard.writeSync(data || '🦄');
|
|
84
168
|
logger.info(`copied to clipboard`, clipboard.readSync());
|
package/src/server/runtime.js
CHANGED
|
@@ -14,7 +14,6 @@ import { MailerProvider } from '../mailer/MailerProvider.js';
|
|
|
14
14
|
import { DataBaseProvider } from '../db/DataBaseProvider.js';
|
|
15
15
|
import { createPeerServer } from './peer.js';
|
|
16
16
|
import { Lampp } from '../runtime/lampp/Lampp.js';
|
|
17
|
-
import { Xampp } from '../runtime/xampp/Xampp.js';
|
|
18
17
|
import { createValkeyConnection } from './valkey.js';
|
|
19
18
|
import { applySecurity, authMiddlewareFactory } from './auth.js';
|
|
20
19
|
import { getInstanceContext } from './conf.js';
|
|
@@ -99,6 +98,8 @@ const buildRuntime = async () => {
|
|
|
99
98
|
return next();
|
|
100
99
|
});
|
|
101
100
|
|
|
101
|
+
if (process.env.NODE_ENV === 'production') app.set('trust proxy', true);
|
|
102
|
+
|
|
102
103
|
app.use((req, res, next) => {
|
|
103
104
|
requestCounter.inc({
|
|
104
105
|
instance: `${host}:${port}${path}`,
|
|
@@ -161,6 +162,15 @@ const buildRuntime = async () => {
|
|
|
161
162
|
continue;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// Flag swagger requests before security middleware is applied
|
|
166
|
+
const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
167
|
+
const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
|
|
168
|
+
if (fs.existsSync(swaggerJsonPath))
|
|
169
|
+
app.use(swaggerPath, (req, res, next) => {
|
|
170
|
+
res.locals.isSwagger = true;
|
|
171
|
+
next();
|
|
172
|
+
});
|
|
173
|
+
|
|
164
174
|
// security
|
|
165
175
|
applySecurity(app, {
|
|
166
176
|
origin: origins.concat(
|
|
@@ -191,22 +201,14 @@ const buildRuntime = async () => {
|
|
|
191
201
|
if (peer) currentPort++;
|
|
192
202
|
|
|
193
203
|
if (!apiBaseHost) {
|
|
194
|
-
const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
195
204
|
if (fs.existsSync(swaggerJsonPath)) {
|
|
196
|
-
// logger.info('Build swagger serve', swaggerJsonPath);
|
|
197
|
-
|
|
198
205
|
const swaggerInstance =
|
|
199
206
|
(swaggerDoc) =>
|
|
200
207
|
(...args) =>
|
|
201
208
|
swaggerUi.setup(swaggerDoc)(...args);
|
|
202
|
-
|
|
203
209
|
const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
|
|
204
|
-
|
|
205
|
-
app.use(
|
|
206
|
-
`${path === '/' ? `/api-docs` : `${path}/api-docs`}`,
|
|
207
|
-
swaggerUi.serve,
|
|
208
|
-
swaggerInstance(swaggerDoc),
|
|
209
|
-
);
|
|
210
|
+
const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
|
|
211
|
+
app.use(swaggerPath, swaggerUi.serve, swaggerInstance(swaggerDoc));
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
|
|
@@ -308,26 +310,6 @@ const buildRuntime = async () => {
|
|
|
308
310
|
);
|
|
309
311
|
}
|
|
310
312
|
break;
|
|
311
|
-
case 'xampp':
|
|
312
|
-
{
|
|
313
|
-
const { disabled } = await Xampp.createApp({
|
|
314
|
-
port,
|
|
315
|
-
host,
|
|
316
|
-
path,
|
|
317
|
-
directory,
|
|
318
|
-
rootHostPath,
|
|
319
|
-
redirect,
|
|
320
|
-
redirectTarget,
|
|
321
|
-
resetRouter: currentPort === initPort,
|
|
322
|
-
});
|
|
323
|
-
if (disabled) continue;
|
|
324
|
-
await UnderpostStartUp.API.listenPortController(
|
|
325
|
-
UnderpostStartUp.API.listenServerFactory(),
|
|
326
|
-
port,
|
|
327
|
-
runningData,
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
break;
|
|
331
313
|
default:
|
|
332
314
|
break;
|
|
333
315
|
}
|
|
@@ -335,7 +317,6 @@ const buildRuntime = async () => {
|
|
|
335
317
|
}
|
|
336
318
|
}
|
|
337
319
|
|
|
338
|
-
if (Xampp.enabled() && Xampp.router) Xampp.initService();
|
|
339
320
|
if (Lampp.enabled() && Lampp.router) Lampp.initService();
|
|
340
321
|
|
|
341
322
|
UnderpostStartUp.API.logRuntimeRouter();
|