@metric-im/administrate 1.1.9 → 1.2.3
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/.claude/settings.local.json +3 -9
- package/multisite.mjs +43 -35
- package/package.json +2 -1
- package/synchronize.mjs +30 -35
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Read(//home/msprague/workspace/
|
|
5
|
-
"Read(//home/msprague/workspace/rootz/
|
|
6
|
-
"Read(//home/msprague/workspace/rootz/epistery/**)",
|
|
7
|
-
"Read(//home/msprague/workspace/rootz/rhonda/node_modules/epistery/**)",
|
|
8
|
-
"Read(//home/msprague/workspace/rootz/rhonda/**)",
|
|
9
|
-
"Read(//home/msprague/workspace/metric-im/account-control/**)",
|
|
10
|
-
"Bash(sed:*)",
|
|
11
|
-
"Bash(git checkout:*)"
|
|
4
|
+
"Read(//home/msprague/workspace/sitewell/**)",
|
|
5
|
+
"Read(//home/msprague/workspace/rootz/data-wallet-demos/**)"
|
|
12
6
|
],
|
|
13
7
|
"deny": [],
|
|
14
8
|
"ask": []
|
|
15
9
|
}
|
|
16
|
-
}
|
|
10
|
+
}
|
package/multisite.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import compression from 'compression';
|
|
2
3
|
import tls from 'tls';
|
|
3
4
|
import { Config } from 'epistery';
|
|
4
5
|
import moment from 'moment';
|
|
@@ -39,7 +40,9 @@ export class MultiSite {
|
|
|
39
40
|
console.log('Emergency cleanup on exit...');
|
|
40
41
|
Object.values(this.sites).forEach((site) => {
|
|
41
42
|
if (site.proc && !site.proc.killed) {
|
|
42
|
-
site.proc.
|
|
43
|
+
try { process.kill(-site.proc.pid, 'SIGKILL'); } catch(e) {
|
|
44
|
+
try { site.proc.kill('SIGKILL'); } catch(e2) {}
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
});
|
|
45
48
|
});
|
|
@@ -76,41 +79,27 @@ export class MultiSite {
|
|
|
76
79
|
async cleanup() {
|
|
77
80
|
console.log('Cleaning up spawned processes...');
|
|
78
81
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}, 15000); // 15 second total timeout
|
|
84
|
-
|
|
85
|
-
const promises = Object.values(this.sites).map(async (site) => {
|
|
82
|
+
// Kill all process groups immediately — no grace period.
|
|
83
|
+
// systemd restart kills and relaunches in quick succession,
|
|
84
|
+
// so async waits just create orphans.
|
|
85
|
+
Object.values(this.sites).forEach((site) => {
|
|
86
86
|
if (site.proc && !site.proc.killed) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
site.proc.kill('SIGTERM');
|
|
97
|
-
|
|
98
|
-
site.proc.on('exit', () => {
|
|
99
|
-
console.log(`${site.name} process terminated gracefully`);
|
|
100
|
-
clearTimeout(timeout);
|
|
101
|
-
resolve();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
87
|
+
const pid = site.proc.pid;
|
|
88
|
+
try {
|
|
89
|
+
process.kill(-pid, 'SIGTERM');
|
|
90
|
+
console.log(`Killed process group for ${site.name} (pgid ${pid})`);
|
|
91
|
+
} catch(e) {
|
|
92
|
+
// Fallback to direct kill if process group kill fails
|
|
93
|
+
try { site.proc.kill('SIGKILL'); } catch(e2) {}
|
|
94
|
+
console.log(`Direct killed ${site.name} (pid ${pid})`);
|
|
95
|
+
}
|
|
104
96
|
}
|
|
105
97
|
});
|
|
106
98
|
|
|
107
|
-
await Promise.all(promises.filter(Boolean));
|
|
108
|
-
|
|
109
99
|
// Clear all health check intervals
|
|
110
100
|
this.healthCheckIntervals.forEach(interval => clearInterval(interval));
|
|
111
101
|
this.healthCheckIntervals.clear();
|
|
112
102
|
|
|
113
|
-
clearTimeout(cleanupTimeout);
|
|
114
103
|
console.log('All spawned processes cleaned up');
|
|
115
104
|
}
|
|
116
105
|
|
|
@@ -133,9 +122,11 @@ export class MultiSite {
|
|
|
133
122
|
clearInterval(intervalId);
|
|
134
123
|
this.healthCheckIntervals.delete(site.name);
|
|
135
124
|
|
|
136
|
-
//
|
|
125
|
+
// Kill the failed process group
|
|
137
126
|
if (site.proc && !site.proc.killed) {
|
|
138
|
-
site.proc.
|
|
127
|
+
try { process.kill(-site.proc.pid, 'SIGTERM'); } catch(e) {
|
|
128
|
+
try { site.proc.kill('SIGTERM'); } catch(e2) {}
|
|
129
|
+
}
|
|
139
130
|
}
|
|
140
131
|
}
|
|
141
132
|
}
|
|
@@ -154,9 +145,11 @@ export class MultiSite {
|
|
|
154
145
|
this.healthCheckIntervals.delete(oldSite.name);
|
|
155
146
|
}
|
|
156
147
|
|
|
157
|
-
// Kill old process
|
|
148
|
+
// Kill old process group
|
|
158
149
|
if (oldSite.proc && !oldSite.proc.killed) {
|
|
159
|
-
oldSite.proc.
|
|
150
|
+
try { process.kill(-oldSite.proc.pid, 'SIGTERM'); } catch(e) {
|
|
151
|
+
try { oldSite.proc.kill('SIGTERM'); } catch(e2) {}
|
|
152
|
+
}
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
// Remove from used ports
|
|
@@ -213,6 +206,17 @@ export class MultiSite {
|
|
|
213
206
|
const instance = new MultiSite(app,options);
|
|
214
207
|
instance.config = new Config();
|
|
215
208
|
|
|
209
|
+
// Compression and security headers for all responses
|
|
210
|
+
app.use(compression());
|
|
211
|
+
app.use((req, res, next) => {
|
|
212
|
+
res.set('X-Content-Type-Options', 'nosniff');
|
|
213
|
+
res.set('X-Frame-Options', 'SAMEORIGIN');
|
|
214
|
+
if (req.secure) {
|
|
215
|
+
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
216
|
+
}
|
|
217
|
+
next();
|
|
218
|
+
});
|
|
219
|
+
|
|
216
220
|
// spawn declared sites
|
|
217
221
|
if (fs.existsSync(resolve('./sites'))) {
|
|
218
222
|
instance.sites = (fs.readdirSync(resolve('./sites'))).reduce((result,hostName)=>{
|
|
@@ -242,8 +246,8 @@ export class MultiSite {
|
|
|
242
246
|
const payload = isBodyMethod ? req.body : null;
|
|
243
247
|
const clientIP = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
|
|
244
248
|
|
|
245
|
-
// Debug logging for POST requests
|
|
246
|
-
if (method === 'POST') {
|
|
249
|
+
// Debug logging for POST requests (set DEBUG_PROXY=1 to enable)
|
|
250
|
+
if (method === 'POST' && process.env.DEBUG_PROXY) {
|
|
247
251
|
console.log(`[Proxy] ${method} ${target}`);
|
|
248
252
|
console.log(`[Proxy] Request body:`, req.body);
|
|
249
253
|
console.log(`[Proxy] Payload:`, payload);
|
|
@@ -381,7 +385,11 @@ export class Site {
|
|
|
381
385
|
console.log(`Spawning site ${this.name} on port ${this.options.env.PORT}`);
|
|
382
386
|
|
|
383
387
|
try {
|
|
384
|
-
this.proc = child_process.spawn('npm', commands,
|
|
388
|
+
this.proc = child_process.spawn('npm', commands, {
|
|
389
|
+
...this.options,
|
|
390
|
+
detached: true,
|
|
391
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
392
|
+
});
|
|
385
393
|
|
|
386
394
|
this.proc.stdout.on('data', (data) => {
|
|
387
395
|
process.stdout.write(`${this.name}: ${data.toString()}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metric-im/administrate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Tools for site administration",
|
|
5
5
|
"homepage": "https://github.com/metric-im/administrate#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@metric-im/componentry": "^1.4.4",
|
|
22
22
|
"acme-client": "^5.4.0",
|
|
23
23
|
"axios": "^1.7.0",
|
|
24
|
+
"compression": "^1.8.1",
|
|
24
25
|
"epistery": "^1.1.4",
|
|
25
26
|
"express": "^5.1.0",
|
|
26
27
|
"moment": "^2.30.1"
|
package/synchronize.mjs
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* code will automatically remain in sync with the given branch of the
|
|
6
6
|
* given repository on Github.
|
|
7
7
|
*
|
|
8
|
+
* The repositoryPath must point to a directory with a .git/config
|
|
9
|
+
* that already has the correct remote configured. No URL discovery
|
|
10
|
+
* or override - git pulls from wherever .git/config says.
|
|
8
11
|
*/
|
|
9
12
|
import fs from 'fs';
|
|
10
13
|
import express from 'express';
|
|
@@ -12,35 +15,27 @@ import {resolve} from "path";
|
|
|
12
15
|
import {spawn, exec} from 'child_process';
|
|
13
16
|
|
|
14
17
|
export class Synchronize {
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} repositoryPath - directory containing .git/config
|
|
20
|
+
* @param {string} [branch] - branch to track (default 'main')
|
|
21
|
+
* @param {function} [authorize] - async (req) => boolean, caller provides auth logic
|
|
22
|
+
*/
|
|
23
|
+
constructor(repositoryPath, branch, authorize) {
|
|
24
|
+
if (!repositoryPath) throw new Error('repositoryPath is required');
|
|
25
|
+
this.repositoryPath = repositoryPath;
|
|
17
26
|
this.branch = branch || 'main';
|
|
27
|
+
this.authorize = authorize;
|
|
18
28
|
this.appName = this.getAppName();
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
static attach(app,branch) {
|
|
22
|
-
const instance = new Synchronize(branch);
|
|
31
|
+
static attach(app, repositoryPath, branch, authorize) {
|
|
32
|
+
const instance = new Synchronize(repositoryPath, branch, authorize);
|
|
23
33
|
app.use('/',instance.routes());
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
discoverRepositoryPath() {
|
|
27
|
-
try {
|
|
28
|
-
const packageJsonPath = resolve(process.cwd(), 'package.json');
|
|
29
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
30
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
31
|
-
if (packageJson.repository) {
|
|
32
|
-
return process.cwd();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.warn('Could not find repository path:', error.message);
|
|
37
|
-
}
|
|
38
|
-
return process.cwd(); // fallback to current directory
|
|
39
|
-
}
|
|
40
|
-
|
|
41
36
|
getAppName() {
|
|
42
37
|
try {
|
|
43
|
-
const packageJsonPath = resolve(this.repositoryPath
|
|
38
|
+
const packageJsonPath = resolve(this.repositoryPath, 'package.json');
|
|
44
39
|
if (fs.existsSync(packageJsonPath)) {
|
|
45
40
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
46
41
|
return packageJson.name;
|
|
@@ -48,16 +43,16 @@ export class Synchronize {
|
|
|
48
43
|
} catch (error) {
|
|
49
44
|
console.warn('Could not extract app name from package.json:', error.message);
|
|
50
45
|
}
|
|
51
|
-
|
|
52
|
-
// Fallback: extract from repository path or current directory
|
|
53
|
-
const repoPath = this.repositoryPath || process.cwd();
|
|
54
|
-
return repoPath.split('/').pop();
|
|
46
|
+
return this.repositoryPath.split('/').pop();
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
routes() {
|
|
58
50
|
const router = new express.Router();
|
|
59
51
|
router.get(/^\/_update/, async (req, res) => {
|
|
60
52
|
try {
|
|
53
|
+
if (this.authorize && !await this.authorize(req)) {
|
|
54
|
+
return res.status(403).json({success: false, error: 'Not authorized'});
|
|
55
|
+
}
|
|
61
56
|
await this.update();
|
|
62
57
|
res.json({success: true, message: 'Update completed successfully'});
|
|
63
58
|
} catch (error) {
|
|
@@ -70,12 +65,12 @@ export class Synchronize {
|
|
|
70
65
|
console.log(`Starting deployment update for ${this.repositoryPath} on branch ${this.branch}`);
|
|
71
66
|
|
|
72
67
|
try {
|
|
73
|
-
// Pull latest changes
|
|
68
|
+
// Pull latest changes - remote is defined in .git/config
|
|
74
69
|
await this.executeCommand('git', ['fetch', 'origin', this.branch]);
|
|
75
70
|
await this.executeCommand('git', ['reset', '--hard', `origin/${this.branch}`]);
|
|
76
71
|
|
|
77
72
|
// Install/update dependencies
|
|
78
|
-
await this.executeCommand('npm', ['install']);
|
|
73
|
+
await this.executeCommand('npm', ['install', '--no-audit', '--no-fund']);
|
|
79
74
|
|
|
80
75
|
// Restart the service using systemctl
|
|
81
76
|
console.log(`Restarting service: ${this.appName}`);
|
|
@@ -91,12 +86,12 @@ export class Synchronize {
|
|
|
91
86
|
executeCommand(command, args) {
|
|
92
87
|
return new Promise((resolve, reject) => {
|
|
93
88
|
console.log(`Executing: ${command} ${args.join(' ')}`);
|
|
94
|
-
const
|
|
95
|
-
stdio: 'inherit',
|
|
96
|
-
cwd: this.repositoryPath
|
|
89
|
+
const child = spawn(command, args, {
|
|
90
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
91
|
+
cwd: this.repositoryPath
|
|
97
92
|
});
|
|
98
93
|
|
|
99
|
-
|
|
94
|
+
child.on('close', (code) => {
|
|
100
95
|
if (code === 0) {
|
|
101
96
|
resolve();
|
|
102
97
|
} else {
|
|
@@ -104,7 +99,7 @@ export class Synchronize {
|
|
|
104
99
|
}
|
|
105
100
|
});
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
child.on('error', (error) => {
|
|
108
103
|
reject(error);
|
|
109
104
|
});
|
|
110
105
|
});
|
|
@@ -125,13 +120,13 @@ export class Synchronize {
|
|
|
125
120
|
});
|
|
126
121
|
}
|
|
127
122
|
static get Package() {
|
|
128
|
-
if (!
|
|
123
|
+
if (!Synchronize._Package) {
|
|
129
124
|
let text = fs.readFileSync(resolve('./package.json'), 'utf8');
|
|
130
|
-
|
|
125
|
+
Synchronize._Package = JSON.parse(text.toString())
|
|
131
126
|
}
|
|
132
|
-
return
|
|
127
|
+
return Synchronize._Package;
|
|
133
128
|
}
|
|
134
129
|
static get Version() {
|
|
135
130
|
return Synchronize.Package.version;
|
|
136
131
|
}
|
|
137
|
-
}
|
|
132
|
+
}
|