lsh-framework 1.1.0 → 1.2.1
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/README.md +70 -4
- package/dist/cli.js +104 -486
- package/dist/commands/doctor.js +427 -0
- package/dist/commands/init.js +371 -0
- package/dist/constants/api.js +94 -0
- package/dist/constants/commands.js +64 -0
- package/dist/constants/config.js +56 -0
- package/dist/constants/database.js +21 -0
- package/dist/constants/errors.js +79 -0
- package/dist/constants/index.js +28 -0
- package/dist/constants/paths.js +28 -0
- package/dist/constants/ui.js +73 -0
- package/dist/constants/validation.js +124 -0
- package/dist/daemon/lshd.js +11 -32
- package/dist/lib/daemon-client-helper.js +7 -4
- package/dist/lib/daemon-client.js +9 -2
- package/dist/lib/format-utils.js +163 -0
- package/dist/lib/job-manager.js +2 -1
- package/dist/lib/platform-utils.js +211 -0
- package/dist/lib/secrets-manager.js +11 -1
- package/dist/lib/string-utils.js +128 -0
- package/dist/services/daemon/daemon-registrar.js +3 -2
- package/dist/services/secrets/secrets.js +154 -30
- package/package.json +10 -74
- package/dist/app.js +0 -33
- package/dist/cicd/analytics.js +0 -261
- package/dist/cicd/auth.js +0 -269
- package/dist/cicd/cache-manager.js +0 -172
- package/dist/cicd/data-retention.js +0 -305
- package/dist/cicd/performance-monitor.js +0 -224
- package/dist/cicd/webhook-receiver.js +0 -640
- package/dist/commands/api.js +0 -346
- package/dist/commands/theme.js +0 -261
- package/dist/commands/zsh-import.js +0 -240
- package/dist/components/App.js +0 -1
- package/dist/components/Divider.js +0 -29
- package/dist/components/REPL.js +0 -43
- package/dist/components/Terminal.js +0 -232
- package/dist/components/UserInput.js +0 -30
- package/dist/daemon/api-server.js +0 -316
- package/dist/daemon/monitoring-api.js +0 -220
- package/dist/lib/api-error-handler.js +0 -185
- package/dist/lib/associative-arrays.js +0 -285
- package/dist/lib/base-api-server.js +0 -290
- package/dist/lib/brace-expansion.js +0 -160
- package/dist/lib/builtin-commands.js +0 -439
- package/dist/lib/executors/builtin-executor.js +0 -52
- package/dist/lib/extended-globbing.js +0 -411
- package/dist/lib/extended-parameter-expansion.js +0 -227
- package/dist/lib/interactive-shell.js +0 -460
- package/dist/lib/job-builtins.js +0 -582
- package/dist/lib/pathname-expansion.js +0 -216
- package/dist/lib/script-runner.js +0 -226
- package/dist/lib/shell-executor.js +0 -2504
- package/dist/lib/shell-parser.js +0 -958
- package/dist/lib/shell-types.js +0 -6
- package/dist/lib/shell.lib.js +0 -40
- package/dist/lib/theme-manager.js +0 -476
- package/dist/lib/variable-expansion.js +0 -385
- package/dist/lib/zsh-compatibility.js +0 -659
- package/dist/lib/zsh-import-manager.js +0 -707
- package/dist/lib/zsh-options.js +0 -328
- package/dist/pipeline/job-tracker.js +0 -491
- package/dist/pipeline/mcli-bridge.js +0 -309
- package/dist/pipeline/pipeline-service.js +0 -1119
- package/dist/pipeline/workflow-engine.js +0 -870
- package/dist/services/api/api.js +0 -58
- package/dist/services/api/auth.js +0 -35
- package/dist/services/api/config.js +0 -7
- package/dist/services/api/file.js +0 -22
- package/dist/services/shell/shell.js +0 -28
- package/dist/services/zapier.js +0 -16
- package/dist/simple-api-server.js +0 -148
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSH Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized location for all hard-coded strings, configuration values,
|
|
5
|
+
* and magic constants used throughout the LSH codebase.
|
|
6
|
+
*
|
|
7
|
+
* This file serves as the single source of truth for all constant values.
|
|
8
|
+
* Import from this file or its submodules instead of using hard-coded strings.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { PATHS, ERRORS, ENV_VARS } from './constants/index.js';
|
|
13
|
+
*
|
|
14
|
+
* // Use constants instead of hard-coded strings
|
|
15
|
+
* const socketPath = PATHS.DAEMON_SOCKET_TEMPLATE.replace('${USER}', process.env.USER);
|
|
16
|
+
* throw new Error(ERRORS.DAEMON_ALREADY_RUNNING);
|
|
17
|
+
* const apiKey = process.env[ENV_VARS.LSH_API_KEY];
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
// Export all constants
|
|
21
|
+
export * from './paths.js';
|
|
22
|
+
export * from './errors.js';
|
|
23
|
+
export * from './commands.js';
|
|
24
|
+
export * from './config.js';
|
|
25
|
+
export * from './api.js';
|
|
26
|
+
export * from './ui.js';
|
|
27
|
+
export * from './validation.js';
|
|
28
|
+
export * from './database.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File paths and system locations
|
|
3
|
+
*
|
|
4
|
+
* All file paths, directories, and system locations used throughout LSH.
|
|
5
|
+
*/
|
|
6
|
+
export const PATHS = {
|
|
7
|
+
// Package and config files
|
|
8
|
+
PACKAGE_JSON_RELATIVE: '../package.json',
|
|
9
|
+
LSHRC_FILENAME: '.lshrc',
|
|
10
|
+
ROOT_DIR: '/',
|
|
11
|
+
// History and session files
|
|
12
|
+
DEFAULT_HISTORY_FILE: '~/.lsh_history',
|
|
13
|
+
// Daemon files (templates with ${USER} placeholder)
|
|
14
|
+
DAEMON_SOCKET_TEMPLATE: '/tmp/lsh-job-daemon-${USER}.sock',
|
|
15
|
+
DAEMON_PID_FILE_TEMPLATE: '/tmp/lsh-job-daemon-${USER}.pid',
|
|
16
|
+
DAEMON_LOG_FILE_TEMPLATE: '/tmp/lsh-job-daemon-${USER}.log',
|
|
17
|
+
DAEMON_JOBS_FILE_TEMPLATE: '/tmp/lsh-daemon-jobs-${USER}.json',
|
|
18
|
+
// Job persistence
|
|
19
|
+
DEFAULT_JOBS_PERSISTENCE_FILE: '/tmp/lsh-jobs.json',
|
|
20
|
+
};
|
|
21
|
+
export const PREFIXES = {
|
|
22
|
+
SESSION_ID: 'lsh_',
|
|
23
|
+
SECRETS_SEED_SUFFIX: 'lsh-secrets',
|
|
24
|
+
};
|
|
25
|
+
export const SYSTEM = {
|
|
26
|
+
UNKNOWN_USER: 'unknown',
|
|
27
|
+
DEFAULT_HOSTNAME: 'localhost',
|
|
28
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI strings and console output
|
|
3
|
+
*
|
|
4
|
+
* All user-facing messages, prompts, and terminal output strings.
|
|
5
|
+
*/
|
|
6
|
+
export const UI_MESSAGES = {
|
|
7
|
+
// Help and usage
|
|
8
|
+
DID_YOU_MEAN: '\nDid you mean one of these?',
|
|
9
|
+
RUN_HELP_MESSAGE: '\nRun \'lsh --help\' to see available commands.',
|
|
10
|
+
// Configuration messages
|
|
11
|
+
CONFIG_EXISTS: 'Configuration file already exists: ${rcFile}',
|
|
12
|
+
CONFIG_CREATED: '✅ Created configuration file: ${rcFile}',
|
|
13
|
+
CONFIG_CREATE_FAILED: '❌ Failed to create configuration: ${message}',
|
|
14
|
+
CONFIG_NOT_FOUND: '❌ Configuration file not found: ${rcFile}',
|
|
15
|
+
CONFIG_INIT_HINT: 'Run "lsh config --init" to create one.',
|
|
16
|
+
CONFIG_FILE_DISPLAY: '📄 Configuration file: ${rcFile}',
|
|
17
|
+
CONFIG_NOT_FOUND_VALIDATE: '❌ Configuration file not found: ${rcFile}',
|
|
18
|
+
CONFIG_VALID: '✅ Configuration file is valid: ${rcFile}',
|
|
19
|
+
CONFIG_HAS_ERRORS: '❌ Configuration file has errors: ${rcFile}',
|
|
20
|
+
// Secrets messages
|
|
21
|
+
FAILED_PUSH_SECRETS: '❌ Failed to push secrets:',
|
|
22
|
+
FAILED_PULL_SECRETS: '❌ Failed to pull secrets:',
|
|
23
|
+
FILE_NOT_FOUND: '❌ File not found: ${envPath}',
|
|
24
|
+
TIP_PULL_FROM_CLOUD: '💡 Tip: Pull from cloud with: lsh pull --env <environment>',
|
|
25
|
+
SECRETS_IN_FILE: '\n📋 Secrets in ${file}:\n',
|
|
26
|
+
TOTAL_SECRETS: '\n Total: ${count} secrets\n',
|
|
27
|
+
FAILED_LIST_SECRETS: '❌ Failed to list secrets:',
|
|
28
|
+
// Version messages
|
|
29
|
+
CURRENT_VERSION: 'Current version:',
|
|
30
|
+
CHECKING_UPDATES: 'Checking npm for updates...',
|
|
31
|
+
FAILED_FETCH_VERSION: '✗ Failed to fetch version information from npm',
|
|
32
|
+
CHECK_INTERNET: '⚠ Make sure you have internet connectivity',
|
|
33
|
+
LATEST_VERSION: 'Latest version:',
|
|
34
|
+
ALREADY_LATEST: '✓ You\'re already on the latest version!',
|
|
35
|
+
VERSION_NEWER: '✓ Your version (${currentVersion}) is newer than npm',
|
|
36
|
+
DEV_VERSION_HINT: 'You may be using a development version',
|
|
37
|
+
UPDATE_AVAILABLE: '⬆ Update available: ${currentVersion} → ${latestVersion}',
|
|
38
|
+
RUN_UPDATE_HINT: 'ℹ Run \'lsh self update\' to install the update',
|
|
39
|
+
};
|
|
40
|
+
export const LOG_MESSAGES = {
|
|
41
|
+
// Environment validation
|
|
42
|
+
VALIDATING_ENV: 'Validating environment configuration',
|
|
43
|
+
ENV_VALIDATION_FAILED: 'Environment validation failed in production',
|
|
44
|
+
// Daemon lifecycle
|
|
45
|
+
DAEMON_STARTING: 'Starting LSH Job Daemon',
|
|
46
|
+
DAEMON_STARTED: 'Daemon started with PID ${pid}',
|
|
47
|
+
DAEMON_STOPPING: 'Stopping LSH Job Daemon',
|
|
48
|
+
DAEMON_STOPPED: 'Daemon stopped',
|
|
49
|
+
// API server
|
|
50
|
+
API_SERVER_STARTED: 'API Server started on port ${port}',
|
|
51
|
+
API_SERVER_STOPPED: 'API Server stopped',
|
|
52
|
+
// Job operations
|
|
53
|
+
ADDING_JOB: 'Adding job: ${name}',
|
|
54
|
+
STARTING_JOB: 'Starting job: ${jobId}',
|
|
55
|
+
TRIGGERING_JOB: 'Triggering job: ${jobId}',
|
|
56
|
+
// Scheduler
|
|
57
|
+
SCHEDULER_STARTING: '📅 Starting job scheduler...',
|
|
58
|
+
SCHEDULER_STARTED: '✅ Job scheduler started successfully',
|
|
59
|
+
// Secrets operations
|
|
60
|
+
WARN_NO_SECRETS_KEY: '⚠️ Warning: No LSH_SECRETS_KEY set. Using machine-derived key.',
|
|
61
|
+
WARN_GENERATE_KEY_MESSAGE: 'To share secrets across machines, generate a key with: lsh secrets key',
|
|
62
|
+
PUSHING_SECRETS: 'Pushing ${envFilePath} to Supabase (${environment})...',
|
|
63
|
+
SECRETS_PUSHED: '✅ Pushed ${count} secrets from ${filename} to Supabase',
|
|
64
|
+
PULLING_SECRETS: 'Pulling ${filename} (${environment}) from Supabase...',
|
|
65
|
+
BACKUP_CREATED: 'Backed up existing .env to ${backup}',
|
|
66
|
+
SECRETS_PULLED: '✅ Pulled ${count} secrets from Supabase',
|
|
67
|
+
};
|
|
68
|
+
export const LOG_LEVELS = {
|
|
69
|
+
INFO: 'INFO',
|
|
70
|
+
WARN: 'WARN',
|
|
71
|
+
ERROR: 'ERROR',
|
|
72
|
+
DEBUG: 'DEBUG',
|
|
73
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation patterns and security rules
|
|
3
|
+
*
|
|
4
|
+
* All validation patterns, security rules, and dangerous command patterns.
|
|
5
|
+
*/
|
|
6
|
+
import { ERRORS, RISK_LEVELS } from './errors.js';
|
|
7
|
+
export const DANGEROUS_PATTERNS = [
|
|
8
|
+
// Critical risk - Filesystem destruction
|
|
9
|
+
{
|
|
10
|
+
pattern: /rm\s+-rf\s+\/(?!\w)/i,
|
|
11
|
+
description: ERRORS.DELETE_ROOT,
|
|
12
|
+
riskLevel: RISK_LEVELS.CRITICAL,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
pattern: /mkfs/i,
|
|
16
|
+
description: ERRORS.MKFS_DETECTED,
|
|
17
|
+
riskLevel: RISK_LEVELS.CRITICAL,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pattern: /dd\s+.*of=/i,
|
|
21
|
+
description: ERRORS.DD_DETECTED,
|
|
22
|
+
riskLevel: RISK_LEVELS.CRITICAL,
|
|
23
|
+
},
|
|
24
|
+
// High risk - Privilege escalation
|
|
25
|
+
{
|
|
26
|
+
pattern: /sudo\s+su/i,
|
|
27
|
+
description: ERRORS.PRIV_ESCALATION,
|
|
28
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /sudo\s+.*passwd/i,
|
|
32
|
+
description: ERRORS.PASSWORD_MOD,
|
|
33
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
34
|
+
},
|
|
35
|
+
// High risk - Remote code execution
|
|
36
|
+
{
|
|
37
|
+
pattern: /curl\s+.*\|\s*bash/i,
|
|
38
|
+
description: ERRORS.REMOTE_EXEC_CURL,
|
|
39
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
pattern: /wget\s+.*\|\s*sh/i,
|
|
43
|
+
description: ERRORS.REMOTE_EXEC_WGET,
|
|
44
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /nc\s+.*-e/i,
|
|
48
|
+
description: ERRORS.REVERSE_SHELL,
|
|
49
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
50
|
+
},
|
|
51
|
+
// High risk - Sensitive file access
|
|
52
|
+
{
|
|
53
|
+
pattern: /cat\s+\/etc\/shadow/i,
|
|
54
|
+
description: ERRORS.READ_SHADOW,
|
|
55
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
pattern: /cat\s+\/etc\/passwd/i,
|
|
59
|
+
description: ERRORS.READ_PASSWD,
|
|
60
|
+
riskLevel: RISK_LEVELS.MEDIUM,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: /\.ssh\/id_rsa/i,
|
|
64
|
+
description: ERRORS.ACCESS_SSH_KEY,
|
|
65
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
66
|
+
},
|
|
67
|
+
// High risk - Process killing
|
|
68
|
+
{
|
|
69
|
+
pattern: /kill\s+-9\s+1\b/i,
|
|
70
|
+
description: ERRORS.KILL_INIT,
|
|
71
|
+
riskLevel: RISK_LEVELS.CRITICAL,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
pattern: /pkill\s+-9\s+.*sshd/i,
|
|
75
|
+
description: ERRORS.KILL_SSHD,
|
|
76
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
77
|
+
},
|
|
78
|
+
// Medium risk - Obfuscation attempts
|
|
79
|
+
{
|
|
80
|
+
pattern: /\$\(.*base64.*\)/i,
|
|
81
|
+
description: ERRORS.BASE64_COMMAND,
|
|
82
|
+
riskLevel: RISK_LEVELS.MEDIUM,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
pattern: /eval.*\$\(/i,
|
|
86
|
+
description: ERRORS.DYNAMIC_EVAL,
|
|
87
|
+
riskLevel: RISK_LEVELS.MEDIUM,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
pattern: /\x00/i,
|
|
91
|
+
description: ERRORS.NULL_BYTE,
|
|
92
|
+
riskLevel: RISK_LEVELS.HIGH,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
export const WARNING_PATTERNS = [
|
|
96
|
+
{
|
|
97
|
+
pattern: /sudo\s+/i,
|
|
98
|
+
description: 'Command uses sudo (elevated privileges)',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
pattern: /rm\s+-rf/i,
|
|
102
|
+
description: 'Force recursive deletion - use with caution',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
pattern: />\s*\/dev\/(sd[a-z]|hd[a-z]|nvme[0-9])/i,
|
|
106
|
+
description: 'Direct disk device access',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
pattern: /chmod\s+777/i,
|
|
110
|
+
description: 'Setting world-writable permissions',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
pattern: /curl.*\|/i,
|
|
114
|
+
description: 'Piping curl output to another command',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
pattern: /wget.*\|/i,
|
|
118
|
+
description: 'Piping wget output to another command',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
pattern: /exec\s+/i,
|
|
122
|
+
description: 'Using exec to replace current process',
|
|
123
|
+
},
|
|
124
|
+
];
|
package/dist/daemon/lshd.js
CHANGED
|
@@ -10,10 +10,10 @@ import * as path from 'path';
|
|
|
10
10
|
import * as net from 'net';
|
|
11
11
|
import { EventEmitter } from 'events';
|
|
12
12
|
import JobManager from '../lib/job-manager.js';
|
|
13
|
-
import { LSHApiServer } from './api-server.js';
|
|
14
13
|
import { validateCommand } from '../lib/command-validator.js';
|
|
15
14
|
import { validateEnvironment, printValidationResults } from '../lib/env-validator.js';
|
|
16
15
|
import { createLogger } from '../lib/logger.js';
|
|
16
|
+
import { getPlatformPaths } from '../lib/platform-utils.js';
|
|
17
17
|
const execAsync = promisify(exec);
|
|
18
18
|
export class LSHJobDaemon extends EventEmitter {
|
|
19
19
|
config;
|
|
@@ -21,18 +21,19 @@ export class LSHJobDaemon extends EventEmitter {
|
|
|
21
21
|
isRunning = false;
|
|
22
22
|
checkTimer;
|
|
23
23
|
logStream;
|
|
24
|
-
ipcServer; // Unix
|
|
24
|
+
ipcServer; // IPC server (Unix sockets or Named Pipes)
|
|
25
25
|
lastRunTimes = new Map(); // Track last run time per job
|
|
26
|
-
apiServer; // API server instance
|
|
27
26
|
logger = createLogger('LSHJobDaemon');
|
|
28
27
|
constructor(config) {
|
|
29
28
|
super();
|
|
30
|
-
|
|
29
|
+
// Use cross-platform paths
|
|
30
|
+
const platformPaths = getPlatformPaths('lsh');
|
|
31
|
+
const jobsFilePath = path.join(platformPaths.tmpDir, `lsh-daemon-jobs-${platformPaths.user}.json`);
|
|
31
32
|
this.config = {
|
|
32
|
-
pidFile:
|
|
33
|
-
logFile:
|
|
34
|
-
jobsFile:
|
|
35
|
-
socketPath:
|
|
33
|
+
pidFile: platformPaths.pidFile,
|
|
34
|
+
logFile: platformPaths.logFile,
|
|
35
|
+
jobsFile: jobsFilePath,
|
|
36
|
+
socketPath: platformPaths.socketPath,
|
|
36
37
|
checkInterval: 2000, // 2 seconds for better cron accuracy
|
|
37
38
|
maxLogSize: 10 * 1024 * 1024, // 10MB
|
|
38
39
|
autoRestart: true,
|
|
@@ -74,28 +75,11 @@ export class LSHJobDaemon extends EventEmitter {
|
|
|
74
75
|
throw new Error('Another daemon instance is already running');
|
|
75
76
|
}
|
|
76
77
|
this.log('INFO', 'Starting LSH Job Daemon');
|
|
77
|
-
// Write PID file
|
|
78
|
-
await fs.promises.writeFile(this.config.pidFile, process.pid.toString());
|
|
78
|
+
// Write PID file with secure permissions (mode 0o600 = rw-------)
|
|
79
|
+
await fs.promises.writeFile(this.config.pidFile, process.pid.toString(), { mode: 0o600 });
|
|
79
80
|
this.isRunning = true;
|
|
80
81
|
this.startJobScheduler();
|
|
81
82
|
this.startIPCServer();
|
|
82
|
-
// Start API server if enabled
|
|
83
|
-
if (this.config.apiEnabled) {
|
|
84
|
-
try {
|
|
85
|
-
this.apiServer = new LSHApiServer(this, {
|
|
86
|
-
port: this.config.apiPort,
|
|
87
|
-
apiKey: this.config.apiKey,
|
|
88
|
-
enableWebhooks: this.config.enableWebhooks,
|
|
89
|
-
webhookEndpoints: this.config.webhookEndpoints
|
|
90
|
-
});
|
|
91
|
-
await this.apiServer.start();
|
|
92
|
-
this.log('INFO', `API Server started on port ${this.config.apiPort}`);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
const err = error;
|
|
96
|
-
this.log('ERROR', `Failed to start API server: ${err.message}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
83
|
// Setup cleanup handlers
|
|
100
84
|
this.setupSignalHandlers();
|
|
101
85
|
this.log('INFO', `Daemon started with PID ${process.pid}`);
|
|
@@ -110,11 +94,6 @@ export class LSHJobDaemon extends EventEmitter {
|
|
|
110
94
|
}
|
|
111
95
|
this.log('INFO', 'Stopping LSH Job Daemon');
|
|
112
96
|
this.isRunning = false;
|
|
113
|
-
// Stop API server if running
|
|
114
|
-
if (this.apiServer) {
|
|
115
|
-
await this.apiServer.stop();
|
|
116
|
-
this.log('INFO', 'API Server stopped');
|
|
117
|
-
}
|
|
118
97
|
if (this.checkTimer) {
|
|
119
98
|
clearInterval(this.checkTimer);
|
|
120
99
|
}
|
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
* Provides wrapper utilities to eliminate repetitive daemon client connection boilerplate
|
|
4
4
|
*/
|
|
5
5
|
import DaemonClient from './daemon-client.js';
|
|
6
|
+
import { getPlatformPaths } from './platform-utils.js';
|
|
6
7
|
/**
|
|
7
|
-
* Default socket path for the daemon
|
|
8
|
+
* Default socket path for the daemon (cross-platform)
|
|
8
9
|
*/
|
|
9
10
|
export function getDefaultSocketPath() {
|
|
10
|
-
|
|
11
|
+
const platformPaths = getPlatformPaths('lsh');
|
|
12
|
+
return platformPaths.socketPath;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
|
-
* Get default user ID
|
|
15
|
+
* Get default user ID (cross-platform)
|
|
14
16
|
*/
|
|
15
17
|
export function getDefaultUserId() {
|
|
16
|
-
|
|
18
|
+
const platformPaths = getPlatformPaths('lsh');
|
|
19
|
+
return platformPaths.user;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Execute an operation with a daemon client, handling all connection boilerplate
|
|
@@ -7,6 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
8
|
import DatabasePersistence from './database-persistence.js';
|
|
9
9
|
import { createLogger } from './logger.js';
|
|
10
|
+
import { getPlatformPaths } from './platform-utils.js';
|
|
10
11
|
export class DaemonClient extends EventEmitter {
|
|
11
12
|
socketPath;
|
|
12
13
|
socket;
|
|
@@ -19,8 +20,14 @@ export class DaemonClient extends EventEmitter {
|
|
|
19
20
|
logger = createLogger('DaemonClient');
|
|
20
21
|
constructor(socketPath, userId) {
|
|
21
22
|
super();
|
|
22
|
-
// Use
|
|
23
|
-
|
|
23
|
+
// Use cross-platform socket path if not provided
|
|
24
|
+
if (!socketPath) {
|
|
25
|
+
const platformPaths = getPlatformPaths('lsh');
|
|
26
|
+
this.socketPath = platformPaths.socketPath;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.socketPath = socketPath;
|
|
30
|
+
}
|
|
24
31
|
this.userId = userId;
|
|
25
32
|
this.sessionId = `lsh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
26
33
|
if (userId) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Utilities for Secret Export
|
|
3
|
+
* Supports multiple output formats: env, json, yaml, toml, export
|
|
4
|
+
*/
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
/**
|
|
7
|
+
* Mask a secret value showing only first 3 and last 3 characters
|
|
8
|
+
*/
|
|
9
|
+
export function maskSecret(value) {
|
|
10
|
+
if (value.length <= 6) {
|
|
11
|
+
return '***';
|
|
12
|
+
}
|
|
13
|
+
return `${value.slice(0, 3)}***${value.slice(-3)}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Apply masking to secrets array
|
|
17
|
+
*/
|
|
18
|
+
export function maskSecrets(secrets) {
|
|
19
|
+
return secrets.map(({ key, value }) => ({
|
|
20
|
+
key,
|
|
21
|
+
value: maskSecret(value),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Detect namespaces from key prefixes for TOML grouping
|
|
26
|
+
* E.g., DATABASE_URL, DATABASE_PORT -> namespace: "database"
|
|
27
|
+
*/
|
|
28
|
+
export function detectNamespaces(secrets) {
|
|
29
|
+
const namespaces = new Map();
|
|
30
|
+
const ungrouped = [];
|
|
31
|
+
// Common prefixes to detect
|
|
32
|
+
const prefixPattern = /^([A-Z][A-Z0-9]*?)_(.+)$/;
|
|
33
|
+
for (const secret of secrets) {
|
|
34
|
+
const match = secret.key.match(prefixPattern);
|
|
35
|
+
if (match) {
|
|
36
|
+
const [, prefix, remainder] = match;
|
|
37
|
+
const namespace = prefix.toLowerCase();
|
|
38
|
+
// Only create namespace if we have multiple keys with same prefix
|
|
39
|
+
if (!namespaces.has(namespace)) {
|
|
40
|
+
namespaces.set(namespace, []);
|
|
41
|
+
}
|
|
42
|
+
namespaces.get(namespace).push({
|
|
43
|
+
key: remainder,
|
|
44
|
+
value: secret.value,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
ungrouped.push(secret);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Filter out single-item namespaces (not worth grouping)
|
|
52
|
+
const filtered = new Map();
|
|
53
|
+
for (const [ns, entries] of namespaces.entries()) {
|
|
54
|
+
if (entries.length > 1) {
|
|
55
|
+
filtered.set(ns, entries);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Move single-entry namespaces back to ungrouped
|
|
59
|
+
ungrouped.push({
|
|
60
|
+
key: `${ns.toUpperCase()}_${entries[0].key}`,
|
|
61
|
+
value: entries[0].value,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Add ungrouped as special namespace if exists
|
|
66
|
+
if (ungrouped.length > 0) {
|
|
67
|
+
filtered.set('_root', ungrouped);
|
|
68
|
+
}
|
|
69
|
+
return filtered;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format secrets as .env file (KEY=value)
|
|
73
|
+
*/
|
|
74
|
+
export function formatAsEnv(secrets) {
|
|
75
|
+
return secrets.map(({ key, value }) => `${key}=${value}`).join('\n');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Format secrets as JSON object
|
|
79
|
+
*/
|
|
80
|
+
export function formatAsJSON(secrets) {
|
|
81
|
+
const obj = {};
|
|
82
|
+
for (const { key, value } of secrets) {
|
|
83
|
+
obj[key] = value;
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(obj, null, 2);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Format secrets as YAML
|
|
89
|
+
*/
|
|
90
|
+
export function formatAsYAML(secrets) {
|
|
91
|
+
const obj = {};
|
|
92
|
+
for (const { key, value } of secrets) {
|
|
93
|
+
obj[key] = value;
|
|
94
|
+
}
|
|
95
|
+
return yaml.dump(obj, {
|
|
96
|
+
lineWidth: -1, // Don't wrap long lines
|
|
97
|
+
noRefs: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Format secrets as TOML with namespace detection
|
|
102
|
+
*/
|
|
103
|
+
export function formatAsTOML(secrets) {
|
|
104
|
+
const namespaces = detectNamespaces(secrets);
|
|
105
|
+
const lines = [];
|
|
106
|
+
// Process root (ungrouped) keys first
|
|
107
|
+
if (namespaces.has('_root')) {
|
|
108
|
+
const rootEntries = namespaces.get('_root');
|
|
109
|
+
for (const { key, value } of rootEntries) {
|
|
110
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
111
|
+
}
|
|
112
|
+
namespaces.delete('_root');
|
|
113
|
+
}
|
|
114
|
+
// Process namespaced keys
|
|
115
|
+
for (const [namespace, entries] of namespaces.entries()) {
|
|
116
|
+
if (lines.length > 0) {
|
|
117
|
+
lines.push(''); // Blank line before section
|
|
118
|
+
}
|
|
119
|
+
lines.push(`[${namespace}]`);
|
|
120
|
+
for (const { key, value } of entries) {
|
|
121
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Format secrets as shell export statements
|
|
128
|
+
*/
|
|
129
|
+
export function formatAsExport(secrets) {
|
|
130
|
+
return secrets
|
|
131
|
+
.map(({ key, value }) => {
|
|
132
|
+
// Escape single quotes in value
|
|
133
|
+
const escapedValue = value.replace(/'/g, "'\\''");
|
|
134
|
+
return `export ${key}='${escapedValue}'`;
|
|
135
|
+
})
|
|
136
|
+
.join('\n');
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Format secrets based on specified format
|
|
140
|
+
*
|
|
141
|
+
* @param secrets - Array of secret entries
|
|
142
|
+
* @param format - Output format
|
|
143
|
+
* @param mask - Whether to mask values (auto-disabled for structured formats unless explicitly set)
|
|
144
|
+
*/
|
|
145
|
+
export function formatSecrets(secrets, format, mask) {
|
|
146
|
+
// Auto-disable masking for structured formats unless explicitly set to true
|
|
147
|
+
const shouldMask = mask ?? (format === 'env');
|
|
148
|
+
const secretsToFormat = shouldMask ? maskSecrets(secrets) : secrets;
|
|
149
|
+
switch (format) {
|
|
150
|
+
case 'env':
|
|
151
|
+
return formatAsEnv(secretsToFormat);
|
|
152
|
+
case 'json':
|
|
153
|
+
return formatAsJSON(secretsToFormat);
|
|
154
|
+
case 'yaml':
|
|
155
|
+
return formatAsYAML(secretsToFormat);
|
|
156
|
+
case 'toml':
|
|
157
|
+
return formatAsTOML(secretsToFormat);
|
|
158
|
+
case 'export':
|
|
159
|
+
return formatAsExport(secretsToFormat);
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dist/lib/job-manager.js
CHANGED
|
@@ -367,7 +367,8 @@ export class JobManager extends BaseJobManager {
|
|
|
367
367
|
const { process: _process, timer: _timer, ...serializable } = job;
|
|
368
368
|
return serializable;
|
|
369
369
|
});
|
|
370
|
-
|
|
370
|
+
// Write with secure permissions (mode 0o600 = rw-------)
|
|
371
|
+
fs.writeFileSync(this.persistenceFile, JSON.stringify(jobs, null, 2), { mode: 0o600 });
|
|
371
372
|
}
|
|
372
373
|
catch (error) {
|
|
373
374
|
this.logger.error('Failed to persist jobs', error);
|