@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.
@@ -1,16 +1,10 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Read(//home/msprague/workspace/rootz/epistery/docs/**)",
5
- "Read(//home/msprague/workspace/rootz/epistery/src/**)",
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.kill('SIGKILL');
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
- // Set a global timeout for the entire cleanup process
80
- const cleanupTimeout = setTimeout(() => {
81
- console.log('Cleanup taking too long, forcing exit...');
82
- process.exit(1);
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
- return new Promise((resolve) => {
88
- const timeout = setTimeout(() => {
89
- if (!site.proc.killed) {
90
- console.log(`Force killing ${site.name} after timeout`);
91
- site.proc.kill('SIGKILL');
92
- }
93
- resolve();
94
- }, 5000); // Reduced to 5 second timeout per process
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
- // Mark the process as failed for cleanup
125
+ // Kill the failed process group
137
126
  if (site.proc && !site.proc.killed) {
138
- site.proc.kill('SIGTERM');
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.kill('SIGTERM');
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, this.options);
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.1.9",
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
- constructor(branch) {
16
- this.repositoryPath = this.discoverRepositoryPath();
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 || process.cwd(), 'package.json');
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 from GitHub
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 process = spawn(command, args, {
95
- stdio: 'inherit',
96
- cwd: this.repositoryPath || process.cwd()
89
+ const child = spawn(command, args, {
90
+ stdio: ['ignore', 'inherit', 'inherit'],
91
+ cwd: this.repositoryPath
97
92
  });
98
93
 
99
- process.on('close', (code) => {
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
- process.on('error', (error) => {
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 (!Syncrhonize._Package) {
123
+ if (!Synchronize._Package) {
129
124
  let text = fs.readFileSync(resolve('./package.json'), 'utf8');
130
- Syncrhonize._Package = JSON.parse(text.toString())
125
+ Synchronize._Package = JSON.parse(text.toString())
131
126
  }
132
- return Syncrhonize._Package;
127
+ return Synchronize._Package;
133
128
  }
134
129
  static get Version() {
135
130
  return Synchronize.Package.version;
136
131
  }
137
- }
132
+ }