@metric-im/administrate 1.0.3 → 1.1.0

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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
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
+ ],
10
+ "deny": [],
11
+ "ask": []
12
+ }
13
+ }
package/certify.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  import Acme from 'acme-client';
3
3
  import tls from 'tls';
4
- import { Config } from './config.mjs';
4
+ import { Config } from 'epistery';
5
5
  import moment from 'moment';
6
6
 
7
7
  const MAX_AGE = 75; // days
@@ -17,9 +17,13 @@ export class Certify {
17
17
  }
18
18
  static async attach(app,options) {
19
19
  const instance = new Certify(app,options);
20
- instance.config = new Config();
21
- if (!instance.config.data.ssl) instance.config.data.ssl = {};
22
- instance.contactEmail = instance.config.profile?.email || instance.options.contactEmail;
20
+ instance.config = new Config(); // Uses default 'epistery' root
21
+
22
+ // Load root config to get contact email
23
+ instance.config.setPath('/');
24
+ instance.config.load();
25
+ instance.contactEmail = instance.options.contactEmail || instance.config.data.ssl?.email || instance.config.data.profile?.email;
26
+
23
27
  instance.acme = new Acme.Client({
24
28
  directoryUrl: Acme.directory.letsencrypt[process.env.PROFILE==='DEV'?'staging':'production'],
25
29
  accountKey: await Acme.crypto.createPrivateKey(),
@@ -71,9 +75,13 @@ export class Certify {
71
75
  if (sitename === 'localhost' || sitename.includes('.local') || sitename.match(/^\d+\.\d+\.\d+\.\d+$/)) {
72
76
  return null; // Use default certificate
73
77
  }
74
-
75
- const site = this.config.data.ssl ? this.config.data.ssl[sitename] : undefined;
76
- if (!site?.certified || moment().isAfter(moment(site.certified).add(MAX_AGE, 'days'))) {
78
+
79
+ // Load domain-specific config to check SSL section
80
+ this.config.setPath(`/${sitename}`);
81
+ this.config.load();
82
+ const sslConfig = this.config.data.ssl;
83
+
84
+ if (!sslConfig?.certified || moment().isAfter(moment(sslConfig.certified).add(MAX_AGE, 'days'))) {
77
85
  if (!this.pending[sitename] || moment().isAfter(moment(this.pending[sitename]).add(MAX_WAIT_TIME, 'seconds'))) {
78
86
  try {
79
87
  this.pending[sitename] = moment();
@@ -91,27 +99,31 @@ export class Certify {
91
99
  }
92
100
  }
93
101
  }
94
- if (site) {
95
- const key = this.config.readFile(site.key).toString();
96
- const cert = this.config.readFile(site.cert).toString();
102
+ if (sslConfig?.key && sslConfig?.cert) {
103
+ const key = this.config.readFile(sslConfig.key).toString();
104
+ const cert = this.config.readFile(sslConfig.cert).toString();
97
105
  return {key: key, cert: cert};
98
106
  } else {
99
107
  return null;
100
108
  }
101
109
  }
102
110
  async renewCert(sitename) {
103
- const site = this.config.data.ssl ? this.config.data.ssl[sitename] : undefined;
104
- if (site?.certified && moment().isBefore(moment(site.certified).add(MAX_AGE, 'days'))) {
111
+ // Load domain config to check SSL certification date
112
+ this.config.setPath(`/${sitename}`);
113
+ this.config.load();
114
+ const sslConfig = this.config.data.ssl;
115
+
116
+ if (sslConfig?.certified && moment().isBefore(moment(sslConfig.certified).add(MAX_AGE, 'days'))) {
105
117
  return;
106
118
  } else {
107
119
  if (!this.contactEmail) throw new Error(`cannot request certificate without CONTACT_EMAIL set`);
108
120
  this.pending[sitename] = true;
109
-
121
+
110
122
  try {
111
123
  // Add timeout wrapper for the entire certificate renewal process
112
124
  await Promise.race([
113
125
  this.doRenewCert(sitename),
114
- new Promise((_, reject) =>
126
+ new Promise((_, reject) =>
115
127
  setTimeout(() => reject(new Error(`Certificate renewal timeout for ${sitename}`)), 120000) // 2 minute timeout
116
128
  )
117
129
  ]);
@@ -128,7 +140,7 @@ export class Certify {
128
140
  const [key, csr] = await Acme.crypto.createCsr({
129
141
  altNames: [sitename],
130
142
  });
131
-
143
+
132
144
  // order certificate with timeout
133
145
  const cert = await this.acme.auto({
134
146
  csr,
@@ -142,14 +154,22 @@ export class Certify {
142
154
  delete this.challenges[challenge.token];
143
155
  },
144
156
  });
145
-
146
- // save certificate
147
- this.config.writeFile(sitename+'.cert', cert);
148
- this.config.writeFile(sitename+'.key', key.toString());
149
- this.config.data.ssl[sitename] = {key:sitename+'.key', cert:sitename+'.cert',certified:moment().format("YYYY-MM-DD")};
157
+
158
+ // Set path to domain directory and save certificate files there
159
+ this.config.setPath(`/${sitename}`);
160
+ this.config.writeFile('ssl_cert.pem', cert);
161
+ this.config.writeFile('ssl_key.pem', key.toString());
162
+
163
+ // Load existing config and add/update [ssl] section
164
+ this.config.load();
165
+ if (!this.config.data.ssl) this.config.data.ssl = {};
166
+ this.config.data.ssl.key = 'ssl_key.pem';
167
+ this.config.data.ssl.cert = 'ssl_cert.pem';
168
+ this.config.data.ssl.certified = moment().format("YYYY-MM-DD");
169
+
150
170
  delete this.pending[sitename];
151
171
  this.config.save();
152
-
172
+
153
173
  console.log(`Certificate successfully renewed for ${sitename}`);
154
174
  }
155
175
  }
package/index.mjs CHANGED
@@ -1,4 +1,3 @@
1
1
  export { Certify } from './certify.mjs';
2
- export { Config, DomainConfig } from './config.mjs';
3
2
  export { Synchronize } from './synchronize.mjs';
4
3
  export { MultiSite, Site } from './multisite.mjs';
package/multisite.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import express from 'express';
2
2
  import tls from 'tls';
3
- import { Config } from './config.mjs';
3
+ import { Config } from 'epistery';
4
4
  import moment from 'moment';
5
5
  import fs from "fs";
6
6
  import {resolve} from "path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metric-im/administrate",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Tools for site administration",
5
5
  "homepage": "https://github.com/metric-im/administrate#readme",
6
6
  "bugs": {
@@ -22,7 +22,9 @@
22
22
  "acme-client": "^5.4.0",
23
23
  "axios": "^1.7.0",
24
24
  "express": "^5.1.0",
25
- "ini": "^5.0.0",
26
25
  "moment": "^2.30.1"
26
+ },
27
+ "peerDependencies": {
28
+ "epistery": "^1.0.0"
27
29
  }
28
30
  }
package/config.mjs DELETED
@@ -1,95 +0,0 @@
1
- import fs from 'fs';
2
- import ini from 'ini';
3
- import {dirname, join, resolve} from "path";
4
- import * as domain from "node:domain";
5
-
6
- export class Config {
7
- constructor(rootName) {
8
- this.rootName = rootName || 'metric-im';
9
- this.homeDir = (process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME);
10
- this.configDir = join(this.homeDir, '.' + this.rootName);
11
- this.configFile = join(this.configDir, 'config.ini');
12
- if (!fs.existsSync(this.configFile)) {
13
- this.initialize();
14
- } else {
15
- this.load();
16
- }
17
- }
18
-
19
- initialize() {
20
- this.data = ini.decode("");
21
- if (!fs.existsSync(this.configDir)) {
22
- fs.mkdirSync(this.configDir);
23
- }
24
- this.save()
25
- }
26
-
27
- load() {
28
- let fileData = fs.readFileSync(this.configFile);
29
- this.data = ini.decode(fileData.toString());
30
- }
31
-
32
- save() {
33
- let text = ini.stringify(this.data, {});
34
- fs.writeFileSync(this.configFile, text);
35
- }
36
-
37
- readFile(filename) {
38
- return fs.readFileSync(join(this.configDir, filename));
39
- }
40
-
41
- writeFile(filename, data) {
42
- fs.writeFileSync(join(this.configDir, filename), data);
43
- }
44
- }
45
-
46
- export class DomainConfig extends Config {
47
- constructor(rootName,domain) {
48
- super(rootName);
49
- this.domain = domain;
50
- this.domainData = {};
51
- }
52
-
53
- setDomain(domain) {
54
- this.domain = domain;
55
- this.domainConfigDir = join(this.configDir, domain);
56
- this.domainConfigFile = join(this.domainConfigDir, 'config.ini');
57
- if (!fs.existsSync(this.domainConfigDir)) {
58
- fs.mkdirSync(this.domainConfigDir);
59
- fs.writeFileSync(this.domainConfigFile,`name=${domain}`);
60
- }
61
- this.load();
62
- }
63
- getDomain(domain) {
64
- let filename = join(this.configDir, domain, 'config.ini');
65
- if (fs.existsSync(filename)) {
66
- const text = fs.readFileSync(filename);
67
- return ini.decode(text.toString());
68
- } else {
69
- return null;
70
- }
71
- }
72
- load() {
73
- if (this.domain) {
74
- const text = fs.readFileSync(this.domainConfigFile);
75
- this.domainData = ini.decode(text.toString());
76
- }
77
- super.load();
78
- }
79
- save() {
80
- if (this.domain) {
81
- const text = ini.stringify(this.domainData);
82
- fs.writeFileSync(this.domainConfigFile, text);
83
- }
84
- super.save();
85
- }
86
- readFile(filename) {
87
- if (this.domain) return fs.readFileSync(join(this.domainConfigDir, filename));
88
- else return super.readFile(filename);
89
- }
90
-
91
- writeFile(filename, data) {
92
- if (this.domain) fs.writeFileSync(join(this.domainConfigDir, filename), data);
93
- else return super.writeFile(filename);
94
- }
95
- }