@cocreate/server 1.2.1 → 1.5.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.
- package/.github/workflows/automated.yml +25 -13
- package/.github/workflows/manual.yml +1 -2
- package/CHANGELOG.md +39 -0
- package/package.json +3 -3
- package/release.config.js +12 -4
- package/src/getInfrastructure.js +362 -0
- package/src/getIp.js +113 -0
- package/src/index.js +392 -31
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
name: Automated Workflow
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
branches:
|
|
5
6
|
- master
|
|
7
|
+
|
|
6
8
|
jobs:
|
|
7
9
|
about:
|
|
8
10
|
runs-on: ubuntu-latest
|
|
@@ -18,27 +20,37 @@ jobs:
|
|
|
18
20
|
with:
|
|
19
21
|
direction: overwrite-github
|
|
20
22
|
githubToken: "${{ secrets.GITHUB }}"
|
|
23
|
+
|
|
21
24
|
release:
|
|
22
25
|
runs-on: ubuntu-latest
|
|
23
26
|
steps:
|
|
24
27
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
26
|
-
- name: Setup Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
+
uses: actions/checkout@v4
|
|
28
29
|
with:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
fetch-depth: 0 # Required so semantic-release can trace git tags/history
|
|
31
|
+
|
|
32
|
+
- name: Setup Node.js
|
|
33
|
+
uses: actions/setup-node@v4
|
|
33
34
|
with:
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
node-version: 22
|
|
36
|
+
|
|
37
|
+
- name: Install Semantic Release & Plugins
|
|
38
|
+
# Installs semantic-release and its plugins on the runner
|
|
39
|
+
run: |
|
|
40
|
+
npm install -g semantic-release \
|
|
41
|
+
@semantic-release/changelog \
|
|
42
|
+
@semantic-release/npm \
|
|
43
|
+
@semantic-release/github \
|
|
36
44
|
@semantic-release/git
|
|
37
|
-
|
|
45
|
+
|
|
46
|
+
- name: Run Semantic Release (Native)
|
|
47
|
+
id: semantic
|
|
48
|
+
# This will automatically pick up your export default config file in the repository root
|
|
49
|
+
run: npx semantic-release
|
|
38
50
|
env:
|
|
39
|
-
GITHUB_TOKEN: "${{ secrets.
|
|
51
|
+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
40
52
|
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
|
53
|
+
|
|
41
54
|
outputs:
|
|
42
55
|
new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
|
|
43
|
-
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
|
44
|
-
|
|
56
|
+
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
# [1.5.0](https://github.com/CoCreate-app/CoCreate-server/compare/v1.4.0...v1.5.0) (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* enhance worker configuration to reserve CPU core for master process ([0a05a51](https://github.com/CoCreate-app/CoCreate-server/commit/0a05a516b4917cafd508a81443855fb5956348bc))
|
|
7
|
+
* replace Metrics with ServerTelemetry for enhanced performance monitoring ([c1de789](https://github.com/CoCreate-app/CoCreate-server/commit/c1de789aa938bc1fa07b95e40728c4fe348ace6b))
|
|
8
|
+
* replace SocketMesh with ServerMesh and add ServerAutoscaler for improved orchestration ([02ac503](https://github.com/CoCreate-app/CoCreate-server/commit/02ac503f709a6d8d4dd680f5d660fc7025cecb1d))
|
|
9
|
+
* set workerId for single Primary process and ensure SocketMesh initialization ([d80084a](https://github.com/CoCreate-app/CoCreate-server/commit/d80084af3e2b72ffa30157be9091d0110302d882))
|
|
10
|
+
|
|
11
|
+
# [1.4.0](https://github.com/CoCreate-app/CoCreate-server/compare/v1.3.0...v1.4.0) (2026-07-16)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* enhance CoCreateServer with IPC routing and edge request handling ([4da9381](https://github.com/CoCreate-app/CoCreate-server/commit/4da9381214806b022c129a2d1a25a1a7ad0a6276))
|
|
17
|
+
* update automated workflow and release configuration for improved semantic release handling ([8d442c2](https://github.com/CoCreate-app/CoCreate-server/commit/8d442c243f05a9daa8cdcd3241f26f4dfb18022b))
|
|
18
|
+
|
|
19
|
+
# [1.3.0](https://github.com/CoCreate-app/CoCreate-server/compare/v1.2.2...v1.3.0) (2026-07-15)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* add infrastructure and IP resolution modules for cloud provider metadata retrieval ([bb7b6cf](https://github.com/CoCreate-app/CoCreate-server/commit/bb7b6cf06a8dcbae0f68c8d3c644ffc40d553d97))
|
|
25
|
+
* enhance server initialization with improved configuration handling and dynamic module imports ([9bd44c3](https://github.com/CoCreate-app/CoCreate-server/commit/9bd44c32ad3be2274b53a0fc859bb05346c34f91))
|
|
26
|
+
* integrate Acme, Nginx, SocketMesh, and Metrics modules for enhanced server capabilities ([1d84a1c](https://github.com/CoCreate-app/CoCreate-server/commit/1d84a1c061d8421433fcb93d183e768d63eada10))
|
|
27
|
+
|
|
28
|
+
## [1.2.2](https://github.com/CoCreate-app/CoCreate-server/compare/v1.2.1...v1.2.2) (2026-07-14)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* refactor server initialization to include API server and improve module loading ([97fe5bf](https://github.com/CoCreate-app/CoCreate-server/commit/97fe5bfa94b98020d042024e3969312684dd7a68))
|
|
34
|
+
* removed post install ([7ecf61a](https://github.com/CoCreate-app/CoCreate-server/commit/7ecf61ae0e5dd37244ecdbfd9c1cf434ef36e676))
|
|
35
|
+
* semantic version handling. Reorganize .gitignore for improved clarity and structure ([27f9506](https://github.com/CoCreate-app/CoCreate-server/commit/27f9506ddb71641dc93b32cef6224a194c8bd009))
|
|
36
|
+
* Update GitHub Actions workflow to use latest versions of actions and Node.js ([043f46e](https://github.com/CoCreate-app/CoCreate-server/commit/043f46e0a3a16c8186ebc7189e00fdfbffd201f7))
|
|
37
|
+
* update module export to ES6 syntax in release.config.js ([2643676](https://github.com/CoCreate-app/CoCreate-server/commit/2643676e58b1ceec917da4780e569b55d65d5abb))
|
|
38
|
+
* update package.json and refactor index.js for module compatibility ([0e21383](https://github.com/CoCreate-app/CoCreate-server/commit/0e21383964b0280f020d9da82058133e7b4d7eed))
|
|
39
|
+
|
|
1
40
|
## [1.2.1](https://github.com/CoCreate-app/CoCreate-server/compare/v1.2.0...v1.2.1) (2025-05-01)
|
|
2
41
|
|
|
3
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A dynamic SSL certificate management and generation tool for proxies like NGINX, with a fallback to Node.js SSL termination. It seamlessly integrates HTTP, HTTPS, and ACME protocols to ensure secure, encrypted connections.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ssl-certificate-management",
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "npx webpack --config webpack.config.js",
|
|
22
22
|
"build": "npx webpack --mode=production --config webpack.config.js",
|
|
23
|
-
"dev": "npx webpack --config webpack.config.js --watch"
|
|
24
|
-
"postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
|
|
23
|
+
"dev": "npx webpack --config webpack.config.js --watch"
|
|
25
24
|
},
|
|
26
25
|
"repository": {
|
|
27
26
|
"type": "git",
|
|
@@ -37,6 +36,7 @@
|
|
|
37
36
|
"type": "GitHub Sponsors ❤",
|
|
38
37
|
"url": "https://github.com/sponsors/CoCreate-app"
|
|
39
38
|
},
|
|
39
|
+
"type": "module",
|
|
40
40
|
"main": "./src/index.js",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@cocreate/acme": "^1.3.0"
|
package/release.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default {
|
|
2
2
|
dryRun: false,
|
|
3
3
|
branches: ["master"],
|
|
4
4
|
plugins: [
|
|
@@ -10,12 +10,20 @@ module.exports = {
|
|
|
10
10
|
changelogFile: "CHANGELOG.md",
|
|
11
11
|
},
|
|
12
12
|
],
|
|
13
|
-
"@semantic-release/npm",
|
|
14
|
-
"@semantic-release/github",
|
|
13
|
+
"@semantic-release/npm",
|
|
15
14
|
[
|
|
16
|
-
"@semantic-release/
|
|
15
|
+
"@semantic-release/github",
|
|
17
16
|
{
|
|
17
|
+
successComment: false,
|
|
18
|
+
failTitle: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"@semantic-release/git",
|
|
23
|
+
{
|
|
24
|
+
// Only stage and commit the changelog and package.json
|
|
18
25
|
assets: ["CHANGELOG.md", "package.json"],
|
|
26
|
+
message: "chore(release): ${nextRelease.version} [skip ci]",
|
|
19
27
|
},
|
|
20
28
|
],
|
|
21
29
|
],
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import https from 'node:https';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
|
|
23
|
+
const METADATA_IP = '169.254.169.254';
|
|
24
|
+
const PROBE_TIMEOUT_MS = 500; // Fast timeout for parallel background probes
|
|
25
|
+
|
|
26
|
+
/* STREAMING_CHUNK: Structuring the native network request wrapper... */
|
|
27
|
+
/**
|
|
28
|
+
* Native, lightweight HTTP request wrapper supporting standard GET/PUT/POST operations.
|
|
29
|
+
*/
|
|
30
|
+
function request(options, data = null, useHttps = false) {
|
|
31
|
+
const client = useHttps ? https : http;
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const req = client.request(options, (res) => {
|
|
34
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
35
|
+
reject(new Error(`Status Code: ${res.statusCode}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let body = '';
|
|
39
|
+
res.setEncoding('utf8');
|
|
40
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
41
|
+
res.on('end', () => { resolve(body); });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
req.on('error', (err) => { reject(err); });
|
|
45
|
+
req.setTimeout(PROBE_TIMEOUT_MS, () => {
|
|
46
|
+
req.destroy();
|
|
47
|
+
reject(new Error('Request Timeout'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (data) {
|
|
51
|
+
req.write(data);
|
|
52
|
+
}
|
|
53
|
+
req.end();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* STREAMING_CHUNK: Configuring AWS metadata IMDSv2 probe... */
|
|
58
|
+
/**
|
|
59
|
+
* AWS IMDSv2 Probe: Acquires session token and retrieves instance metadata.
|
|
60
|
+
*/
|
|
61
|
+
async function probeAWS() {
|
|
62
|
+
const token = await request({
|
|
63
|
+
host: METADATA_IP,
|
|
64
|
+
path: '/latest/api/token',
|
|
65
|
+
method: 'PUT',
|
|
66
|
+
headers: { 'X-aws-ec2-metadata-token-ttl-seconds': '60' }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const docStr = await request({
|
|
70
|
+
host: METADATA_IP,
|
|
71
|
+
path: '/latest/dynamic/instance-identity/document',
|
|
72
|
+
method: 'GET',
|
|
73
|
+
headers: { 'X-aws-ec2-metadata-token': token }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const doc = JSON.parse(docStr);
|
|
77
|
+
return {
|
|
78
|
+
provider: 'aws',
|
|
79
|
+
region: doc.region,
|
|
80
|
+
zone: doc.availabilityZone,
|
|
81
|
+
instanceId: doc.instanceId,
|
|
82
|
+
instanceType: doc.instanceType
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* STREAMING_CHUNK: Configuring Google Cloud (GCP) metadata probe... */
|
|
87
|
+
/**
|
|
88
|
+
* GCP Metadata Probe: Requests instance data with the mandatory Flavor header.
|
|
89
|
+
*/
|
|
90
|
+
async function probeGCP() {
|
|
91
|
+
const docStr = await request({
|
|
92
|
+
host: 'metadata.google.internal',
|
|
93
|
+
path: '/computeMetadata/v1/instance/?recursive=true',
|
|
94
|
+
method: 'GET',
|
|
95
|
+
headers: { 'Metadata-Flavor': 'Google' }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const doc = JSON.parse(docStr);
|
|
99
|
+
const zone = doc.zone ? doc.zone.split('/').pop() : 'unknown';
|
|
100
|
+
const region = zone.slice(0, zone.lastIndexOf('-'));
|
|
101
|
+
const instanceType = doc.machineType ? doc.machineType.split('/').pop() : 'unknown';
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
provider: 'gcp',
|
|
105
|
+
region,
|
|
106
|
+
zone,
|
|
107
|
+
instanceId: doc.id ? String(doc.id) : 'unknown',
|
|
108
|
+
instanceType
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* STREAMING_CHUNK: Configuring Azure metadata probe... */
|
|
113
|
+
/**
|
|
114
|
+
* Microsoft Azure Probe: Requests metadata using the validation header.
|
|
115
|
+
*/
|
|
116
|
+
async function probeAzure() {
|
|
117
|
+
const docStr = await request({
|
|
118
|
+
host: METADATA_IP,
|
|
119
|
+
path: '/metadata/instance?api-version=2021-02-01',
|
|
120
|
+
method: 'GET',
|
|
121
|
+
headers: { 'Metadata': 'true' }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const doc = JSON.parse(docStr);
|
|
125
|
+
const compute = doc.compute || {};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
provider: 'azure',
|
|
129
|
+
region: compute.location || 'unknown',
|
|
130
|
+
zone: compute.zone || 'unknown',
|
|
131
|
+
instanceId: compute.vmId || 'unknown',
|
|
132
|
+
instanceType: compute.vmSize || 'unknown'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* STREAMING_CHUNK: Configuring DigitalOcean metadata probe... */
|
|
137
|
+
/**
|
|
138
|
+
* DigitalOcean Probe: Requests droplet metadata configuration.
|
|
139
|
+
*/
|
|
140
|
+
async function probeDigitalOcean() {
|
|
141
|
+
const docStr = await request({
|
|
142
|
+
host: METADATA_IP,
|
|
143
|
+
path: '/metadata/v1.json',
|
|
144
|
+
method: 'GET'
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const doc = JSON.parse(docStr);
|
|
148
|
+
return {
|
|
149
|
+
provider: 'digitalocean',
|
|
150
|
+
region: doc.region || 'unknown',
|
|
151
|
+
zone: doc.region ? `${doc.region}-a` : 'unknown',
|
|
152
|
+
instanceId: doc.droplet_id ? String(doc.droplet_id) : 'unknown',
|
|
153
|
+
instanceType: doc.size || 'unknown'
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* STREAMING_CHUNK: Configuring Oracle Cloud (OCI) metadata probe... */
|
|
158
|
+
/**
|
|
159
|
+
* Oracle Cloud (OCI) Probe: Validates via the Bearer Token header required by IMDSv2.
|
|
160
|
+
*/
|
|
161
|
+
async function probeOCI() {
|
|
162
|
+
const docStr = await request({
|
|
163
|
+
host: METADATA_IP,
|
|
164
|
+
path: '/opc/v2/instance/',
|
|
165
|
+
method: 'GET',
|
|
166
|
+
headers: { 'Authorization': 'Bearer Oracle' }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const doc = JSON.parse(docStr);
|
|
170
|
+
return {
|
|
171
|
+
provider: 'oracle',
|
|
172
|
+
region: doc.region || 'unknown',
|
|
173
|
+
zone: doc.faultDomain || 'unknown',
|
|
174
|
+
instanceId: doc.id || 'unknown',
|
|
175
|
+
instanceType: doc.shape || 'unknown'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* STREAMING_CHUNK: Configuring Hetzner Cloud metadata probe... */
|
|
180
|
+
/**
|
|
181
|
+
* Hetzner Cloud Probe: Requests basic instance parameters on Hetzner routes.
|
|
182
|
+
*/
|
|
183
|
+
async function probeHetzner() {
|
|
184
|
+
const docStr = await request({
|
|
185
|
+
host: METADATA_IP,
|
|
186
|
+
path: '/hetzner/v1/metadata',
|
|
187
|
+
method: 'GET'
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const lines = docStr.split('\n');
|
|
191
|
+
const meta = {};
|
|
192
|
+
for (const line of lines) {
|
|
193
|
+
const splitIdx = line.indexOf(':');
|
|
194
|
+
if (splitIdx !== -1) {
|
|
195
|
+
const key = line.slice(0, splitIdx).trim();
|
|
196
|
+
const val = line.slice(splitIdx + 1).trim();
|
|
197
|
+
meta[key] = val;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const zone = meta['availability-zone'] || 'unknown';
|
|
202
|
+
const region = meta['region'] || (zone ? zone.slice(0, zone.lastIndexOf('-')) : 'unknown');
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
provider: 'hetzner',
|
|
206
|
+
region,
|
|
207
|
+
zone,
|
|
208
|
+
instanceId: meta['instance-id'] || 'unknown',
|
|
209
|
+
instanceType: 'cloud-vm'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* STREAMING_CHUNK: Configuring Vultr and Equinix metadata probes... */
|
|
214
|
+
/**
|
|
215
|
+
* Vultr Probe: Pulls metadata configurations from standard OpenStack mapping paths.
|
|
216
|
+
*/
|
|
217
|
+
async function probeVultr() {
|
|
218
|
+
const idStr = await request({
|
|
219
|
+
host: METADATA_IP,
|
|
220
|
+
path: '/v1/instance-v2-id',
|
|
221
|
+
method: 'GET'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const hostStr = await request({
|
|
225
|
+
host: METADATA_IP,
|
|
226
|
+
path: '/v1/hostname',
|
|
227
|
+
method: 'GET'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
provider: 'vultr',
|
|
232
|
+
region: 'vultr-global',
|
|
233
|
+
zone: 'vultr-zone',
|
|
234
|
+
instanceId: idStr ? idStr.trim() : 'unknown',
|
|
235
|
+
instanceType: hostStr ? hostStr.trim() : 'vultr-instance'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Equinix Metal Probe: Requests local bare metal profile metadata via direct secure gateway.
|
|
241
|
+
*/
|
|
242
|
+
async function probeEquinix() {
|
|
243
|
+
const docStr = await request({
|
|
244
|
+
host: 'metadata.platformequinix.com',
|
|
245
|
+
path: '/metadata',
|
|
246
|
+
method: 'GET'
|
|
247
|
+
}, null, true);
|
|
248
|
+
|
|
249
|
+
const doc = JSON.parse(docStr);
|
|
250
|
+
return {
|
|
251
|
+
provider: 'equinix-metal',
|
|
252
|
+
region: doc.metro || doc.facility || 'unknown',
|
|
253
|
+
zone: doc.facility || 'unknown',
|
|
254
|
+
instanceId: doc.id || 'unknown',
|
|
255
|
+
instanceType: doc.plan || 'baremetal-metal'
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* STREAMING_CHUNK: Resolving local physical hardware profiles... */
|
|
260
|
+
/**
|
|
261
|
+
* Safely inspects Linux OS profiles to identify local Bare-Metal / Physical system models.
|
|
262
|
+
*/
|
|
263
|
+
function resolveBareMetalIdentity() {
|
|
264
|
+
try {
|
|
265
|
+
if (fs.existsSync('/sys/class/dmi/id/product_name')) {
|
|
266
|
+
const productName = fs.readFileSync('/sys/class/dmi/id/product_name', 'utf8').trim();
|
|
267
|
+
const vendorName = fs.existsSync('/sys/class/dmi/id/sys_vendor')
|
|
268
|
+
? fs.readFileSync('/sys/class/dmi/id/sys_vendor', 'utf8').trim()
|
|
269
|
+
: 'BareMetal';
|
|
270
|
+
|
|
271
|
+
let isVirtual = false;
|
|
272
|
+
const virtualClues = ['qemu', 'kvm', 'virtualbox', 'vmware', 'xen'];
|
|
273
|
+
for (const clue of virtualClues) {
|
|
274
|
+
if (productName.toLowerCase().includes(clue) || vendorName.toLowerCase().includes(clue)) {
|
|
275
|
+
isVirtual = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!isVirtual) {
|
|
280
|
+
return {
|
|
281
|
+
provider: 'baremetal',
|
|
282
|
+
region: 'on-premise',
|
|
283
|
+
zone: 'on-premise-datacenter',
|
|
284
|
+
instanceId: productName.replace(/\s+/g, '-').toLowerCase(),
|
|
285
|
+
instanceType: `${vendorName}-${productName}`.replace(/\s+/g, '-').toLowerCase()
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
// Non-blocking fallback
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* STREAMING_CHUNK: Running the infallible infrastructure resolver... */
|
|
296
|
+
/**
|
|
297
|
+
* Dynamically resolves the host's physical infrastructure environment.
|
|
298
|
+
* Runs exactly once during the server boot sequence. Guaranteed to never throw.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} masterInstanceId - The pre-configured fallback instance ID
|
|
301
|
+
* @returns {Promise<Object>} Resolved server.infrastructure payload
|
|
302
|
+
*/
|
|
303
|
+
export async function getInfrastructure(masterInstanceId) {
|
|
304
|
+
const localFallback = {
|
|
305
|
+
provider: 'local',
|
|
306
|
+
region: 'local',
|
|
307
|
+
zone: 'local',
|
|
308
|
+
instanceId: masterInstanceId,
|
|
309
|
+
instanceType: 'local'
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
// Phase 1: Concurrently race cloud metadata IMDS probes
|
|
314
|
+
try {
|
|
315
|
+
const cloudMetadata = await Promise.any([
|
|
316
|
+
probeAWS(),
|
|
317
|
+
probeGCP(),
|
|
318
|
+
probeAzure(),
|
|
319
|
+
probeDigitalOcean(),
|
|
320
|
+
probeOCI(),
|
|
321
|
+
probeHetzner(),
|
|
322
|
+
probeVultr(),
|
|
323
|
+
probeEquinix()
|
|
324
|
+
]);
|
|
325
|
+
if (cloudMetadata) {
|
|
326
|
+
return cloudMetadata;
|
|
327
|
+
}
|
|
328
|
+
} catch (err) {
|
|
329
|
+
// Proceed to local sweeps
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Phase 2: Detect local On-Prem Bare Metal Hardware
|
|
333
|
+
try {
|
|
334
|
+
const bareMetalInfo = resolveBareMetalIdentity();
|
|
335
|
+
if (bareMetalInfo) {
|
|
336
|
+
bareMetalInfo.instanceId = masterInstanceId;
|
|
337
|
+
return bareMetalInfo;
|
|
338
|
+
}
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// Proceed to env checks
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Phase 3: Check Environment Variable Overrides
|
|
344
|
+
if (process.env.PROVIDER) {
|
|
345
|
+
return {
|
|
346
|
+
provider: process.env.PROVIDER,
|
|
347
|
+
region: process.env.REGION || 'unknown',
|
|
348
|
+
zone: process.env.ZONE || 'unknown',
|
|
349
|
+
instanceId: process.env.INSTANCE_ID || masterInstanceId,
|
|
350
|
+
instanceType: process.env.INSTANCE_TYPE || 'unknown'
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
} catch (globalErr) {
|
|
355
|
+
// Suppress any unexpected error contexts and fall back cleanly
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Phase 4: Developer Machine Fallback
|
|
359
|
+
return localFallback;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export default getInfrastructure;
|
package/src/getIp.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
|
|
18
|
+
import https from 'node:https';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
|
|
21
|
+
// Prioritized list of reliable public IP lookup APIs
|
|
22
|
+
const IP_PROVIDERS = [
|
|
23
|
+
'https://api.ipify.org',
|
|
24
|
+
'https://icanhazip.com',
|
|
25
|
+
'https://ifconfig.me/ip',
|
|
26
|
+
'https://ipinfo.io/ip'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initiates an HTTPS GET request to a specific IP provider with a hard timeout.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} url - The URL of the IP lookup provider
|
|
33
|
+
* @param {number} timeoutMs - Max execution limit before canceling the promise
|
|
34
|
+
* @returns {Promise<string>} - Resolves with the plain-text IP address
|
|
35
|
+
*/
|
|
36
|
+
function fetchIpFromProvider(url, timeoutMs = 3000) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const req = https.get(url, (res) => {
|
|
39
|
+
if (res.statusCode !== 200) {
|
|
40
|
+
reject(new Error(`Non-200 response code: ${res.statusCode}`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
46
|
+
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
const cleanIp = data.trim();
|
|
49
|
+
const ipv4Regex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
|
50
|
+
if (ipv4Regex.test(cleanIp)) {
|
|
51
|
+
resolve(cleanIp);
|
|
52
|
+
} else {
|
|
53
|
+
reject(new Error(`Returned invalid IP structure: "${cleanIp}"`));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
req.on('error', (err) => { reject(err); });
|
|
59
|
+
|
|
60
|
+
req.setTimeout(timeoutMs, () => {
|
|
61
|
+
req.destroy();
|
|
62
|
+
reject(new Error(`Request timed out`));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Scans the local machine's physical network adapters to find the primary local IP.
|
|
69
|
+
* Used as a fallback if the machine has no active internet connection or is fully offline.
|
|
70
|
+
*
|
|
71
|
+
* @returns {string} - Returns the first valid local IPv4 address, or '127.0.0.1'
|
|
72
|
+
*/
|
|
73
|
+
export function getLocalIPFallback() {
|
|
74
|
+
try {
|
|
75
|
+
const interfaces = os.networkInterfaces();
|
|
76
|
+
for (const devName in interfaces) {
|
|
77
|
+
const face = interfaces[devName];
|
|
78
|
+
for (let i = 0; i < face.length; i++) {
|
|
79
|
+
const alias = face[i];
|
|
80
|
+
// Check for standard IPv4 and exclude typical loopbacks
|
|
81
|
+
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
|
82
|
+
return alias.address;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Safe, non-blocking fallback context
|
|
88
|
+
}
|
|
89
|
+
return '127.0.0.1';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Robust universal public IP resolution function.
|
|
94
|
+
* Iterates through highly redundant public IP lookup services.
|
|
95
|
+
* Guaranteed to never throw errors.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Promise<string>} - The resolved public IP (or local IP if completely offline)
|
|
98
|
+
*/
|
|
99
|
+
export async function getIp() {
|
|
100
|
+
for (const url of IP_PROVIDERS) {
|
|
101
|
+
try {
|
|
102
|
+
const ip = await fetchIpFromProvider(url);
|
|
103
|
+
return ip;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Suppress fallback exceptions to attempt the next endpoint seamlessly
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Offline / Air-Gapped Fallback
|
|
110
|
+
return getLocalIPFallback();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default getIp;
|
package/src/index.js
CHANGED
|
@@ -1,38 +1,399 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
********************************************************************************/
|
|
18
|
+
|
|
19
|
+
import { EventEmitter } from 'node:events';
|
|
20
|
+
import Http from 'node:http';
|
|
21
|
+
import Https from 'node:https';
|
|
22
|
+
import Acme from '@cocreate/acme';
|
|
23
|
+
import ServerMesh from '@cocreate/server-mesh';
|
|
24
|
+
import ServerAutoscaler from '@cocreate/server-autoscaler';
|
|
25
|
+
import ServerTelemetry from '@cocreate/server-telemetry';
|
|
26
|
+
|
|
27
|
+
import cluster from 'node:cluster';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import uid from '@cocreate/uuid';
|
|
30
|
+
import Config from '@cocreate/config';
|
|
31
|
+
import authenticate from '@cocreate/authenticate';
|
|
32
|
+
import authorize from '@cocreate/authorize';
|
|
33
|
+
|
|
34
|
+
import SocketServer from '@cocreate/socket-server';
|
|
35
|
+
import CrudServer from '@cocreate/crud-server';
|
|
36
|
+
import LazyLoader from '@cocreate/lazy-loader';
|
|
37
|
+
import FileServer from '@cocreate/file-server';
|
|
38
|
+
import ApiServer from '@cocreate/api';
|
|
39
|
+
|
|
40
|
+
import { ObjectId } from '@cocreate/utils';
|
|
41
|
+
|
|
42
|
+
// Dynamic environmental resolvers
|
|
43
|
+
import resolveInfrastructure from './getInfrastructure.js';
|
|
44
|
+
import getIP from './getIp.js';
|
|
45
|
+
|
|
46
|
+
const isPrimary = cluster.isPrimary !== undefined ? cluster.isPrimary : cluster.isMaster;
|
|
47
|
+
const id = process.env.SERVER_ID || ObjectId().toString();
|
|
48
|
+
|
|
49
|
+
const sharedMap = new Map();
|
|
50
|
+
const pendingPromises = new Map();
|
|
51
|
+
const activeWorkers = new Map();
|
|
52
|
+
|
|
53
|
+
export class CoCreateServer extends EventEmitter {
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this.id = id;
|
|
57
|
+
this.isPrimary = isPrimary;
|
|
58
|
+
|
|
59
|
+
if (isPrimary) {
|
|
60
|
+
this.workers = activeWorkers;
|
|
61
|
+
} else {
|
|
62
|
+
this.workerId = parseInt(process.env.WORKER_ID, 10) || 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.totalWorkers = 0;
|
|
66
|
+
this.workerStorage = null;
|
|
67
|
+
this.isInitialized = false;
|
|
68
|
+
|
|
69
|
+
this.organization_id = null;
|
|
70
|
+
this.host = null;
|
|
71
|
+
this.ip = null;
|
|
72
|
+
this.infrastructure = null;
|
|
73
|
+
|
|
74
|
+
// Create HTTP server for edge proxy ACME challenges and secure HTTPS redirects
|
|
75
|
+
this.http = Http.createServer((req, res) => {
|
|
76
|
+
const requestUrl = req.url || '/';
|
|
77
|
+
// 1. Allow ACME challenges to pass through over unencrypted HTTP
|
|
78
|
+
if (requestUrl.startsWith('/.well-known/acme-challenge/')) {
|
|
79
|
+
return this.handleEdgeRequest(req, res);
|
|
80
|
+
}
|
|
81
|
+
// 2. Force a 301 redirect to HTTPS for all other traffic
|
|
82
|
+
const host = req.headers.host ? req.headers.host.split(':')[0] : 'localhost';
|
|
83
|
+
res.writeHead(301, { "Location": `https://${host}${requestUrl}` });
|
|
84
|
+
res.end();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.https = null;
|
|
88
|
+
this.mesh = null;
|
|
89
|
+
this.autoscaler = null;
|
|
90
|
+
this.authenticate = authenticate;
|
|
91
|
+
this.authorize = authorize;
|
|
92
|
+
this.wsManager = null;
|
|
93
|
+
this.crud = null;
|
|
94
|
+
this.telemetry = null;
|
|
95
|
+
this.files = null;
|
|
96
|
+
this.loader = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
send(payload) {
|
|
100
|
+
if (!payload || !payload.method) {
|
|
101
|
+
console.error("[@cocreate/server] Cannot route IPC send without a 'method' definition.");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.isPrimary) {
|
|
106
|
+
// --- MASTER SEND ROUTING PATHWAY ---
|
|
107
|
+
// 1. Dispatch locally on Master process unless explicitly bypassed
|
|
108
|
+
if (payload.master !== false) {
|
|
109
|
+
this.emit(payload.method, payload);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Compile target worker list dynamically using a flat ternary condition.
|
|
113
|
+
// This eliminates duplicate loops, nested blocks, and repeated try/catch statements.
|
|
114
|
+
const targetIds = payload.broadcast === true
|
|
115
|
+
? Array.from(this.workers.keys())
|
|
116
|
+
: (Array.isArray(payload.workers) ? payload.workers : []);
|
|
117
|
+
|
|
118
|
+
// 3. Dispatch to all compiled destination targets in a single unified loop.
|
|
119
|
+
for (const wId of targetIds) {
|
|
120
|
+
const worker = this.workers.get(wId);
|
|
121
|
+
if (worker?.process && typeof worker.process.send === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
worker.process.send({ ipcPayload: true, payload });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`[@cocreate/server] Failed sending to worker ${wId}:`, err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// --- WORKER SEND ROUTING PATHWAY ---
|
|
131
|
+
// 1. Emit locally on this worker so native listeners trigger immediately
|
|
132
|
+
this.emit(payload.method, payload);
|
|
133
|
+
|
|
134
|
+
// 2. Relay the message upstream to the Primary Orchestrator Process
|
|
135
|
+
if (typeof process.send === 'function') {
|
|
136
|
+
try {
|
|
137
|
+
process.send({ ipcPayload: true, payload });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error("[@cocreate/server] Failed sending upstream IPC payload to Master:", err);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async init() {
|
|
146
|
+
if (this.isInitialized) {
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.isInitialized = true;
|
|
151
|
+
setupIPC(this);
|
|
152
|
+
|
|
153
|
+
this.workerStorage = {
|
|
154
|
+
set: (key, value) => workerStorageManager(this, 'set', key, value),
|
|
155
|
+
get: (key) => workerStorageManager(this, 'get', key),
|
|
156
|
+
delete: (key) => workerStorageManager(this, 'delete', key),
|
|
157
|
+
size: () => workerStorageManager(this, 'size'),
|
|
158
|
+
clear: () => workerStorageManager(this, 'clear')
|
|
18
159
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
160
|
+
|
|
161
|
+
const config = await Config({
|
|
162
|
+
'organization_id': '',
|
|
163
|
+
'host': '',
|
|
164
|
+
'workers': ''
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.organization_id = config.organization_id || null;
|
|
168
|
+
this.host = config.host || null;
|
|
169
|
+
|
|
170
|
+
let configuredWorkers = config.workers;
|
|
171
|
+
|
|
172
|
+
if (configuredWorkers !== false && configuredWorkers !== 'false') {
|
|
173
|
+
const parsedWorkers = parseInt(configuredWorkers, 10);
|
|
174
|
+
if (parsedWorkers === -1) {
|
|
175
|
+
// Use total CPU cores minus 1 (minimum of 1) to reserve one core for the master process
|
|
176
|
+
this.totalWorkers = Math.max(1, os.cpus().length - 1);
|
|
177
|
+
} else {
|
|
178
|
+
this.totalWorkers = parsedWorkers || os.cpus().length;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Explicitly set workerId = 1 on the single Primary process if workers configuration specifies no clustering (<= 1)
|
|
183
|
+
if (this.isPrimary && this.totalWorkers <= 1) {
|
|
184
|
+
this.workerId = 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// DB interface is booted first so that ServerMesh can cleanly read nodes and write startup heartbeats
|
|
188
|
+
this.crud = await CrudServer.init(this);
|
|
189
|
+
|
|
190
|
+
// Assign the ServerMesh globally. This guarantees both Workers and Primary can safely call "this.mesh.send"
|
|
191
|
+
this.mesh = ServerMesh;
|
|
192
|
+
this.autoscaler = ServerAutoscaler;
|
|
193
|
+
|
|
194
|
+
if (this.isPrimary) {
|
|
195
|
+
console.log(`[@cocreate/server] [PRIMARY] Orchestrator starting...`);
|
|
196
|
+
|
|
197
|
+
this.ip = await getIP();
|
|
198
|
+
this.infrastructure = await resolveInfrastructure(this.id);
|
|
199
|
+
await this.mesh.init(this);
|
|
200
|
+
await this.autoscaler.init(this);
|
|
201
|
+
|
|
202
|
+
if (this.totalWorkers > 1) {
|
|
203
|
+
console.log(`[@cocreate/server] [PRIMARY] Orchestrator running with PID: ${process.pid}`);
|
|
204
|
+
for (let i = 0; i < this.totalWorkers; i++) {
|
|
205
|
+
const currentWorkerId = i + 1;
|
|
206
|
+
const worker = cluster.fork({
|
|
207
|
+
WORKER_ID: currentWorkerId,
|
|
208
|
+
SERVER_ID: this.id,
|
|
209
|
+
SERVER_IP: this.ip,
|
|
210
|
+
SERVER_INFRASTRUCTURE: JSON.stringify(this.infrastructure)
|
|
211
|
+
});
|
|
212
|
+
activeWorkers.set(currentWorkerId, {
|
|
213
|
+
id: currentWorkerId,
|
|
214
|
+
pid: worker.process.pid,
|
|
215
|
+
process: worker
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Supervise and automatically reboot dead or crashed worker processes
|
|
220
|
+
cluster.on('exit', (worker, code, signal) => {
|
|
221
|
+
console.log(`[@cocreate/server] [PRIMARY] Worker ${worker.process.pid} exited (Code: ${code}, Signal: ${signal})`);
|
|
222
|
+
let deadWorkerId = null;
|
|
223
|
+
for (let [id, w] of activeWorkers.entries()) {
|
|
224
|
+
if (w.pid === worker.process.pid) {
|
|
225
|
+
deadWorkerId = id;
|
|
226
|
+
activeWorkers.delete(id);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const targetId = deadWorkerId || (activeWorkers.size + 1);
|
|
231
|
+
|
|
232
|
+
const newWorker = cluster.fork({
|
|
233
|
+
WORKER_ID: targetId,
|
|
234
|
+
SERVER_ID: this.id,
|
|
235
|
+
SERVER_IP: this.ip,
|
|
236
|
+
SERVER_INFRASTRUCTURE: JSON.stringify(this.infrastructure)
|
|
237
|
+
});
|
|
238
|
+
activeWorkers.set(targetId, {
|
|
239
|
+
id: targetId,
|
|
240
|
+
pid: newWorker.process.pid,
|
|
241
|
+
process: newWorker
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// In clustered mode, only the Primary exits early. The workers handle the core traffic.
|
|
246
|
+
if (this.totalWorkers > 1) {
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.ip = process.env.SERVER_IP || await getIP();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
this.infrastructure = process.env.SERVER_INFRASTRUCTURE
|
|
256
|
+
? JSON.parse(process.env.SERVER_INFRASTRUCTURE)
|
|
257
|
+
: await resolveInfrastructure(this.id);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(`[@cocreate/server] Worker ${process.pid} failed to parse pre-resolved infrastructure. Falling back...`, error);
|
|
260
|
+
this.infrastructure = await resolveInfrastructure(this.id);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Initialize HTTPS & Ephemeral certificate via ACME
|
|
264
|
+
this.certificate = Acme;
|
|
265
|
+
this.https = Https.createServer({
|
|
266
|
+
SNICallback: (domain, cb) => this.certificate.sniCallback(domain, cb)
|
|
267
|
+
}, (req, res) => this.handleEdgeRequest(req, res));
|
|
268
|
+
await this.certificate.init(this);
|
|
269
|
+
|
|
270
|
+
// Standard core services
|
|
271
|
+
this.wsManager = new SocketServer(this);
|
|
272
|
+
this.api = await ApiServer.init(this);
|
|
273
|
+
this.files = FileServer.init(this);
|
|
274
|
+
this.loader = await LazyLoader.init(this);
|
|
275
|
+
|
|
276
|
+
// Initialize performance telemetry
|
|
277
|
+
this.telemetry = await ServerTelemetry.init(this);
|
|
278
|
+
|
|
279
|
+
// Bind directly to standard web ports (Requires setcap or root privileges)
|
|
280
|
+
const HTTP_PORT = process.env.HTTP_PORT || 80;
|
|
281
|
+
const HTTPS_PORT = process.env.HTTPS_PORT || 443;
|
|
282
|
+
const BIND_IP = process.env.BIND_IP || '0.0.0.0';
|
|
283
|
+
const processLabel = this.isPrimary ? `Primary (${process.pid})` : `Worker ${process.pid} (ID: ${this.workerId})`;
|
|
284
|
+
|
|
285
|
+
if (this.http) {
|
|
286
|
+
this.http.listen(HTTP_PORT, BIND_IP, () => {
|
|
287
|
+
console.log(`[@cocreate/server] ${processLabel} HTTP listening on ${BIND_IP}:${HTTP_PORT} (Redirects & ACME)`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (this.https) {
|
|
292
|
+
this.https.listen(HTTPS_PORT, BIND_IP, () => {
|
|
293
|
+
console.log(`[@cocreate/server] ${processLabel} HTTPS listening on ${BIND_IP}:${HTTPS_PORT} (Secure Edge & WSS)`);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return this;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async handleEdgeRequest(req, res) {
|
|
301
|
+
try {
|
|
302
|
+
const requestUrl = req.url || '/';
|
|
303
|
+
if (requestUrl.startsWith("/webhooks/")) {
|
|
304
|
+
if (this.loader) return this.loader.request(req, res);
|
|
305
|
+
}
|
|
306
|
+
if (this.files) return this.files.send(req, res);
|
|
307
|
+
|
|
308
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
309
|
+
res.end("Internal Error: Files component missing");
|
|
310
|
+
} catch (error) {
|
|
311
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
312
|
+
res.end("Invalid request routing context");
|
|
313
|
+
}
|
|
22
314
|
}
|
|
23
315
|
}
|
|
24
316
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
317
|
+
function executeLocalWorkerStorageOperation(method, key, value) {
|
|
318
|
+
switch (method) {
|
|
319
|
+
case 'set': sharedMap.set(key, value); return true;
|
|
320
|
+
case 'get': return sharedMap.get(key);
|
|
321
|
+
case 'delete': return sharedMap.delete(key);
|
|
322
|
+
case 'size': return sharedMap.size;
|
|
323
|
+
case 'clear': sharedMap.clear(); return true;
|
|
324
|
+
default: throw new Error(`Unsupported storage key operation: "${method}"`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function workerStorageManager(serverCtx, method, key, value) {
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
try {
|
|
331
|
+
if (serverCtx.isPrimary) {
|
|
332
|
+
resolve(executeLocalWorkerStorageOperation(method, key, value));
|
|
333
|
+
} else {
|
|
334
|
+
const messageId = uid.generate();
|
|
335
|
+
pendingPromises.set(messageId, { resolve, reject });
|
|
336
|
+
process.send({ id: serverCtx.id, messageId, method, key, value });
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
reject(error);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function setupIPC(serverCtx) {
|
|
345
|
+
if (serverCtx.isPrimary) {
|
|
346
|
+
cluster.on('message', (worker, message) => {
|
|
347
|
+
// 1. Check for standard storage operations
|
|
348
|
+
if (message && message.id === serverCtx.id) {
|
|
349
|
+
try {
|
|
350
|
+
message.value = executeLocalWorkerStorageOperation(message.method, message.key, message.value);
|
|
351
|
+
worker.send(message);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
message.error = error.error;
|
|
354
|
+
worker.send(message);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// 2. Routing logic for CoCreate Unified IPC Payloads
|
|
358
|
+
else if (message && message.ipcPayload && message.payload) {
|
|
359
|
+
const { payload } = message;
|
|
360
|
+
|
|
361
|
+
// Track sending worker PID to worker mapping
|
|
362
|
+
let senderWorkerId = null;
|
|
363
|
+
for (const [wId, w] of serverCtx.workers.entries()) {
|
|
364
|
+
if (w.pid === worker.process.pid) {
|
|
365
|
+
senderWorkerId = wId;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Expose sender metadata to routing logic
|
|
371
|
+
payload.senderWorkerId = senderWorkerId;
|
|
372
|
+
|
|
373
|
+
// Pass it off to the primary orchestrator's central send method
|
|
374
|
+
serverCtx.send(payload);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
process.on('message', (message) => {
|
|
379
|
+
// 1. Check for standard storage operations
|
|
380
|
+
if (message && message.id === serverCtx.id) {
|
|
381
|
+
const pending = pendingPromises.get(message.messageId);
|
|
382
|
+
if (pending) {
|
|
383
|
+
pendingPromises.delete(message.messageId);
|
|
384
|
+
if (message.error) {
|
|
385
|
+
pending.reject(new Error(message.error));
|
|
386
|
+
} else {
|
|
387
|
+
pending.resolve(message.value);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// 2. Incoming Unified IPC payload from Master -> Worker Process
|
|
392
|
+
else if (message && message.ipcPayload && message.payload) {
|
|
393
|
+
serverCtx.emit(message.payload.method, message.payload);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
35
396
|
}
|
|
36
397
|
}
|
|
37
398
|
|
|
38
|
-
|
|
399
|
+
export default new CoCreateServer();
|