underpost 2.8.881 → 2.8.882
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 +46 -36
- 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/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 +189 -85
- package/src/client/components/core/LoadingAnimation.js +5 -10
- package/src/client/components/core/Modal.js +255 -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/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 -10
- package/test/crypto.test.js +117 -0
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
|
@@ -99,6 +99,8 @@ const buildRuntime = async () => {
|
|
|
99
99
|
return next();
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
if (process.env.NODE_ENV === 'production') app.set('trust proxy', true);
|
|
103
|
+
|
|
102
104
|
app.use((req, res, next) => {
|
|
103
105
|
requestCounter.inc({
|
|
104
106
|
instance: `${host}:${port}${path}`,
|
|
@@ -161,6 +163,15 @@ const buildRuntime = async () => {
|
|
|
161
163
|
continue;
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
// Flag swagger requests before security middleware is applied
|
|
167
|
+
const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
168
|
+
const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
|
|
169
|
+
if (fs.existsSync(swaggerJsonPath))
|
|
170
|
+
app.use(swaggerPath, (req, res, next) => {
|
|
171
|
+
res.locals.isSwagger = true;
|
|
172
|
+
next();
|
|
173
|
+
});
|
|
174
|
+
|
|
164
175
|
// security
|
|
165
176
|
applySecurity(app, {
|
|
166
177
|
origin: origins.concat(
|
|
@@ -191,22 +202,14 @@ const buildRuntime = async () => {
|
|
|
191
202
|
if (peer) currentPort++;
|
|
192
203
|
|
|
193
204
|
if (!apiBaseHost) {
|
|
194
|
-
const swaggerJsonPath = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
|
|
195
205
|
if (fs.existsSync(swaggerJsonPath)) {
|
|
196
|
-
// logger.info('Build swagger serve', swaggerJsonPath);
|
|
197
|
-
|
|
198
206
|
const swaggerInstance =
|
|
199
207
|
(swaggerDoc) =>
|
|
200
208
|
(...args) =>
|
|
201
209
|
swaggerUi.setup(swaggerDoc)(...args);
|
|
202
|
-
|
|
203
210
|
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
|
-
);
|
|
211
|
+
const swaggerPath = `${path === '/' ? `/api-docs` : `${path}/api-docs`}`;
|
|
212
|
+
app.use(swaggerPath, swaggerUi.serve, swaggerInstance(swaggerDoc));
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
if (db && apis) await DataBaseProvider.load({ apis, host, path, db });
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module crypto.test
|
|
3
|
+
* @description Unit tests for SymmetricCrypto and AsymmetricCrypto classes
|
|
4
|
+
* in the crypto module.
|
|
5
|
+
* * Uses 'chai' for assertions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Import Chai's assertion library
|
|
9
|
+
import { expect } from 'chai';
|
|
10
|
+
|
|
11
|
+
// Import the cryptographic classes from the Canvas's refactored module
|
|
12
|
+
import { SymmetricCrypto, AsymmetricCrypto } from '../src/server/crypto.js';
|
|
13
|
+
|
|
14
|
+
// Define a common plaintext message for testing
|
|
15
|
+
const plaintext = 'This is a secret message for testing cryptographic operations.';
|
|
16
|
+
|
|
17
|
+
// --- Main Test Suite ---
|
|
18
|
+
|
|
19
|
+
describe('Crypto Module Tests', () => {
|
|
20
|
+
// --- SymmetricCrypto Tests (AES-256-CBC) ---
|
|
21
|
+
describe('SymmetricCrypto (AES-256-CBC)', () => {
|
|
22
|
+
/**
|
|
23
|
+
* Test case: Verify that key and IV are automatically generated.
|
|
24
|
+
*/
|
|
25
|
+
it('should generate new 32-byte key and 16-byte IV if none are provided', () => {
|
|
26
|
+
const symm = new SymmetricCrypto();
|
|
27
|
+
// Key should be 32 bytes (64 hex characters) and IV 16 bytes (32 hex characters)
|
|
28
|
+
expect(symm.encryptionKeyHex).to.be.a('string').and.have.lengthOf(64);
|
|
29
|
+
expect(symm.ivHex).to.be.a('string').and.have.lengthOf(32);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Test case: Encrypt data and ensure successful decryption back to the original plaintext.
|
|
34
|
+
*/
|
|
35
|
+
it('should encrypt and successfully decrypt data', () => {
|
|
36
|
+
const symm = new SymmetricCrypto();
|
|
37
|
+
const ciphertext = symm.encryptData(plaintext);
|
|
38
|
+
|
|
39
|
+
// Ciphertext should contain IV and the encrypted data, separated by a colon
|
|
40
|
+
expect(ciphertext).to.include(':');
|
|
41
|
+
|
|
42
|
+
const decryptedText = symm.decryptData(ciphertext);
|
|
43
|
+
expect(decryptedText).to.equal(plaintext);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Test case: Verify that decryption fails gracefully if the ciphertext is tampered with (e.g., corrupting the encrypted payload).
|
|
48
|
+
*
|
|
49
|
+
* FIX: We now reliably tamper with the encrypted hex string by removing the last character.
|
|
50
|
+
* This ensures the underlying crypto operation fails due to invalid hex length or incomplete data,
|
|
51
|
+
* guaranteeing the try/catch block in the implementation is hit, and the expected error is thrown.
|
|
52
|
+
*/
|
|
53
|
+
it('should fail decryption gracefully for tampered ciphertext (invalid payload)', () => {
|
|
54
|
+
const symm = new SymmetricCrypto();
|
|
55
|
+
const ciphertext = symm.encryptData(plaintext);
|
|
56
|
+
|
|
57
|
+
const [ivHex, encryptedHex] = ciphertext.split(':');
|
|
58
|
+
|
|
59
|
+
// Tamper with the encrypted content by cutting off the last character.
|
|
60
|
+
const tamperedEncryptedHex = encryptedHex.substring(0, encryptedHex.length - 1);
|
|
61
|
+
|
|
62
|
+
const tamperedCiphertext = `${ivHex}:${tamperedEncryptedHex}`;
|
|
63
|
+
|
|
64
|
+
// Expect the internal error handling to throw the generic error message
|
|
65
|
+
expect(() => symm.decryptData(tamperedCiphertext)).to.throw(
|
|
66
|
+
Error,
|
|
67
|
+
'Decryption failed. Check key, IV, or ciphertext integrity.',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// --- AsymmetricCrypto Tests (RSA 2048) ---
|
|
73
|
+
describe('AsymmetricCrypto (RSA 2048)', () => {
|
|
74
|
+
/**
|
|
75
|
+
* Test case: Verify that RSA key pair is automatically generated.
|
|
76
|
+
*/
|
|
77
|
+
it('should generate a new RSA key pair if none are provided', () => {
|
|
78
|
+
const asymm = new AsymmetricCrypto();
|
|
79
|
+
// Public and Private keys should be PEM strings
|
|
80
|
+
expect(asymm.publicKey).to.be.a('string').and.include('BEGIN PUBLIC KEY');
|
|
81
|
+
expect(asymm.privateKey).to.be.a('string').and.include('BEGIN PRIVATE KEY');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Test case: Encrypt with public key and decrypt with the corresponding private key.
|
|
86
|
+
*/
|
|
87
|
+
it('should encrypt data with public key and decrypt with private key', () => {
|
|
88
|
+
const asymm = new AsymmetricCrypto();
|
|
89
|
+
const ciphertext = asymm.encryptData(plaintext);
|
|
90
|
+
|
|
91
|
+
// Ciphertext is a hex string
|
|
92
|
+
expect(ciphertext).to.be.a('string');
|
|
93
|
+
|
|
94
|
+
const decryptedText = asymm.decryptData(ciphertext);
|
|
95
|
+
expect(decryptedText).to.equal(plaintext);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Test case: Verify that decryption fails gracefully when using a mismatched private key.
|
|
100
|
+
*/
|
|
101
|
+
it('should fail decryption gracefully when using the wrong private key', () => {
|
|
102
|
+
// 1. Generate the key pair and encrypt the data
|
|
103
|
+
const asymm1 = new AsymmetricCrypto();
|
|
104
|
+
const ciphertext = asymm1.encryptData(plaintext);
|
|
105
|
+
|
|
106
|
+
// 2. Generate a completely different key pair (wrong key)
|
|
107
|
+
const asymm2 = new AsymmetricCrypto();
|
|
108
|
+
|
|
109
|
+
// 3. Try to decrypt ciphertext from asymm1 using the private key from asymm2
|
|
110
|
+
// The implementation will log the 'oaep decoding error' and re-throw the generic message.
|
|
111
|
+
expect(() => asymm2.decryptData(ciphertext)).to.throw(
|
|
112
|
+
Error,
|
|
113
|
+
'Decryption failed. Check private key or ciphertext integrity.',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|