@metric-im/administrate 1.0.3 → 1.1.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//home/msprague/workspace/sitewell/**)",
5
+ "Read(//home/msprague/workspace/rootz/data-wallet-demos/**)"
6
+ ],
7
+ "deny": [],
8
+ "ask": []
9
+ }
10
+ }
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
@@ -18,8 +18,13 @@ export class Certify {
18
18
  static async attach(app,options) {
19
19
  const instance = new Certify(app,options);
20
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;
21
+
22
+ // Load root config to get contact email
23
+ instance.config.setPath('/');
24
+ instance.config.load();
25
+
26
+ instance.contactEmail = instance.options.contactEmail || instance.config.data.ssl?.email || instance.config.data.profile?.email;
27
+
23
28
  instance.acme = new Acme.Client({
24
29
  directoryUrl: Acme.directory.letsencrypt[process.env.PROFILE==='DEV'?'staging':'production'],
25
30
  accountKey: await Acme.crypto.createPrivateKey(),
@@ -71,9 +76,13 @@ export class Certify {
71
76
  if (sitename === 'localhost' || sitename.includes('.local') || sitename.match(/^\d+\.\d+\.\d+\.\d+$/)) {
72
77
  return null; // Use default certificate
73
78
  }
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'))) {
79
+
80
+ // Load domain-specific config to check SSL section
81
+ this.config.setPath(`/${sitename}`);
82
+ this.config.load();
83
+ const sslConfig = this.config.data.ssl;
84
+
85
+ if (!sslConfig?.certified || moment().isAfter(moment(sslConfig.certified).add(MAX_AGE, 'days'))) {
77
86
  if (!this.pending[sitename] || moment().isAfter(moment(this.pending[sitename]).add(MAX_WAIT_TIME, 'seconds'))) {
78
87
  try {
79
88
  this.pending[sitename] = moment();
@@ -91,27 +100,31 @@ export class Certify {
91
100
  }
92
101
  }
93
102
  }
94
- if (site) {
95
- const key = this.config.readFile(site.key).toString();
96
- const cert = this.config.readFile(site.cert).toString();
103
+ if (sslConfig?.key && sslConfig?.cert) {
104
+ const key = this.config.readFile(sslConfig.key).toString();
105
+ const cert = this.config.readFile(sslConfig.cert).toString();
97
106
  return {key: key, cert: cert};
98
107
  } else {
99
108
  return null;
100
109
  }
101
110
  }
102
111
  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'))) {
112
+ // Load domain config to check SSL certification date
113
+ this.config.setPath(`/${sitename}`);
114
+ this.config.load();
115
+ const sslConfig = this.config.data.ssl;
116
+
117
+ if (sslConfig?.certified && moment().isBefore(moment(sslConfig.certified).add(MAX_AGE, 'days'))) {
105
118
  return;
106
119
  } else {
107
120
  if (!this.contactEmail) throw new Error(`cannot request certificate without CONTACT_EMAIL set`);
108
121
  this.pending[sitename] = true;
109
-
122
+
110
123
  try {
111
124
  // Add timeout wrapper for the entire certificate renewal process
112
125
  await Promise.race([
113
126
  this.doRenewCert(sitename),
114
- new Promise((_, reject) =>
127
+ new Promise((_, reject) =>
115
128
  setTimeout(() => reject(new Error(`Certificate renewal timeout for ${sitename}`)), 120000) // 2 minute timeout
116
129
  )
117
130
  ]);
@@ -128,7 +141,7 @@ export class Certify {
128
141
  const [key, csr] = await Acme.crypto.createCsr({
129
142
  altNames: [sitename],
130
143
  });
131
-
144
+
132
145
  // order certificate with timeout
133
146
  const cert = await this.acme.auto({
134
147
  csr,
@@ -142,14 +155,22 @@ export class Certify {
142
155
  delete this.challenges[challenge.token];
143
156
  },
144
157
  });
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")};
158
+
159
+ // Set path to domain directory and save certificate files there
160
+ this.config.setPath(`/${sitename}`);
161
+ this.config.writeFile('ssl_cert.pem', cert);
162
+ this.config.writeFile('ssl_key.pem', key.toString());
163
+
164
+ // Load existing config and add/update [ssl] section
165
+ this.config.load();
166
+ if (!this.config.data.ssl) this.config.data.ssl = {};
167
+ this.config.data.ssl.key = 'ssl_key.pem';
168
+ this.config.data.ssl.cert = 'ssl_cert.pem';
169
+ this.config.data.ssl.certified = moment().format("YYYY-MM-DD");
170
+
150
171
  delete this.pending[sitename];
151
172
  this.config.save();
152
-
173
+
153
174
  console.log(`Certificate successfully renewed for ${sitename}`);
154
175
  }
155
176
  }
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.1",
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.3"
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
- }