Skip to content

Commit e8236c2

Browse files
committed
ref: update for 2025
1 parent e76088f commit e8236c2

File tree

4 files changed

+439
-3
lines changed

4 files changed

+439
-3
lines changed

example.env

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,17 @@
1-
ENV=DEVELOPMENT
2-
PORT=3000
1+
#export NODE_ENV='production'
2+
export NODE_ENV='development'
3+
4+
export BIND_ADDRESS='127.0.0.1'
5+
export PORT='3000'
6+
7+
export APP_BASE_URL='https://example.com'
8+
export APP_TIMEZONE='America/Denver'
9+
10+
export GOOGLE_CLIENT_ID='000000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com'
11+
export GOOGLE_CLIENT_SECRET='XXXXXXXXXXXXXXXXXXXXXXXX'
12+
13+
export POSTMARK_SERVER_TOKEN='xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxx57'
14+
15+
export TWILIO_ACCOUNT_SID=ACffffffffffffffffffffffffffffffff
16+
export TWILIO_AUTH_TOKEN=ffffffffffffffffffffffffffffffff
17+
export TWILIO_MESSAGING_SERVICE_SID=MGffffffffffffffffffffffffffffffff

lib/server-state.js

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* @file server-state.js
3+
* @license MPL-2.0
4+
*
5+
* This Source Code Form is subject to the terms of the Mozilla Public
6+
* License, v. 2.0. If a copy of the MPL was not distributed with this
7+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
8+
*/
9+
10+
let ServerState = {};
11+
12+
/** @type {AppInfo} */
13+
ServerState._appInfo = {
14+
hostname: "example.tld",
15+
name: "example-app",
16+
version: "v0.0.0",
17+
publicEnvs: ["NODE_ENV", "PORT"],
18+
secretEnvs: ["DOESNTEXIST"],
19+
};
20+
21+
ServerState.STARTED_AT = Date.now();
22+
ServerState._MASK = "*";
23+
24+
/** @typedef {Number} UInt8 */
25+
/** @typedef {String} DomainString */
26+
/** @typedef {String} UpperString */
27+
/** @typedef {String} VersionString */
28+
29+
/**
30+
* @typedef AppInfo
31+
* @prop {DomainString} hostname
32+
* @prop {String} name
33+
* @prop {VersionString} version
34+
* @prop {Array<UpperString>} publicEnvs
35+
* @prop {Array<UpperString>} secretEnvs
36+
*/
37+
38+
/**
39+
* @param {AppInfo} appInfo
40+
*/
41+
ServerState.init = async function (appInfo) {
42+
Object.assign(ServerState._appInfo, appInfo);
43+
};
44+
45+
/**
46+
* @param {String} str
47+
* @param {UInt8} lastN
48+
*/
49+
ServerState._scrub = function (str, lastN) {
50+
if (lastN > 4) {
51+
throw new Error(
52+
"[SECURITY] Refusing to show more than the last 4 characters of a secret.",
53+
);
54+
}
55+
56+
// return early if we should mask all characters
57+
let visible = lastN || 0;
58+
let shouldMaskAll = visible === 0 || str.length < 8;
59+
if (shouldMaskAll) {
60+
let masked = ServerState._MASK.repeat(str.length);
61+
return masked;
62+
}
63+
64+
// show no more than a quarter of the characters (i.e. last 2 of 8)
65+
let maxVisibleF = str.length / 4;
66+
let maxVisible = Math.floor(maxVisibleF);
67+
visible = Math.min(maxVisible, visible);
68+
69+
// 'my-secret-string' => '**************ng'
70+
let maskLen = str.length + -visible;
71+
let mask = ServerState._MASK.repeat(maskLen);
72+
let lastNChars = str.slice(str.length - visible);
73+
74+
let masked = `${mask}${lastNChars}`;
75+
return masked;
76+
};
77+
78+
/** @type {import('express').Handler} */
79+
ServerState.getEnvs = async function (req, res) {
80+
/** @type {Object.<UpperString, String>} */
81+
let envsMap = {};
82+
83+
let envNames = ServerState._appInfo.publicEnvs.concat(
84+
ServerState._appInfo.secretEnvs,
85+
);
86+
void envNames.sort();
87+
88+
for (let envName of envNames) {
89+
let val = process.env[envName] || "";
90+
let isPublic = ServerState._appInfo.publicEnvs.includes(envName);
91+
if (isPublic) {
92+
envsMap[envName] = val;
93+
continue;
94+
}
95+
96+
// XXX SECURITY -- DO NOT REMOVE `scrub()`
97+
let lastN = 2;
98+
envsMap[envName] = ServerState._scrub(val, lastN);
99+
}
100+
101+
res.setHeader("Content-Type", "application/json");
102+
res.end(JSON.stringify(envsMap, null, 2));
103+
};
104+
105+
/** @type {import('express').Handler} */
106+
ServerState.getPong = async function (req, res) {
107+
let now = Date.now();
108+
let uptimeMs = now - ServerState.STARTED_AT;
109+
110+
let uptimeSecs = uptimeMs / 1000;
111+
{
112+
let uptimeFixed = uptimeSecs.toFixed(3);
113+
uptimeSecs = parseFloat(uptimeFixed);
114+
}
115+
116+
let parts = ServerState._durationToParts(uptimeMs);
117+
118+
let result = {
119+
hostname: ServerState._appInfo.hostname,
120+
uptime_debug: `${parts.hours}h ${parts.minutes}m ${parts.seconds}s`,
121+
uptime_seconds: uptimeSecs,
122+
version: `${ServerState._appInfo.name}/${ServerState._appInfo.version}`,
123+
versions: {
124+
api: ServerState._appInfo.version,
125+
node: process.versions.node,
126+
icu: process.versions.icu,
127+
tz: process.versions.tz,
128+
},
129+
};
130+
131+
res.setHeader("Content-Type", "application/json");
132+
res.end(JSON.stringify(result, null, 2));
133+
};
134+
135+
/**
136+
* Into hour, minute, and second parts
137+
* @param {Number} duration - milliseconds
138+
*/
139+
ServerState._durationToParts = function (duration) {
140+
let uptimeSecs = duration / 1000;
141+
uptimeSecs = Math.floor(uptimeSecs);
142+
143+
let days = 0;
144+
days = uptimeSecs / 86400;
145+
days = Math.floor(days);
146+
uptimeSecs -= days * 86400;
147+
148+
let hours = 0;
149+
hours = uptimeSecs / 3600;
150+
hours = Math.floor(hours);
151+
uptimeSecs -= hours * 3600;
152+
153+
let minute = "00";
154+
let minutes = uptimeSecs / 60;
155+
minutes = Math.floor(minutes);
156+
uptimeSecs -= minutes * 60;
157+
minute = minutes.toString().padStart(2, "0");
158+
159+
let second = "00";
160+
let seconds = uptimeSecs;
161+
seconds = Math.floor(seconds);
162+
second = seconds.toString().padStart(2, "0");
163+
164+
let description = ``;
165+
if (days > 0) {
166+
description = `${days}d `;
167+
}
168+
if (hours > 0) {
169+
description = `${description}${hours}h `;
170+
}
171+
if (minutes > 0) {
172+
description = `${description}${minute}m `;
173+
}
174+
description = `${description}${second}s`;
175+
176+
return {
177+
days: days.toString(),
178+
hours: hours.toString(),
179+
minutes: minute,
180+
seconds: second,
181+
description: description,
182+
};
183+
};
184+
185+
/** @type {import('express').Handler} */
186+
ServerState.getUptime = async function (req, res) {
187+
let now = Date.now();
188+
let uptimeMs = now - ServerState.STARTED_AT;
189+
190+
let uptimeSecs = uptimeMs / 1000;
191+
{
192+
let uptimeFixed = uptimeSecs.toFixed(3);
193+
uptimeSecs = parseFloat(uptimeFixed);
194+
}
195+
196+
let parts = ServerState._durationToParts(uptimeMs);
197+
198+
let result = {
199+
uptime_debug: parts.description,
200+
uptime_seconds: uptimeSecs,
201+
};
202+
203+
res.setHeader("Content-Type", "application/json");
204+
res.end(JSON.stringify(result, null, 2));
205+
};
206+
207+
/** @type {import('express').Handler} */
208+
ServerState.getVersion = async function getVersions(req, res) {
209+
let result = {
210+
version: `${ServerState._appInfo.name}/${ServerState._appInfo.version}`,
211+
};
212+
213+
res.setHeader("Content-Type", "application/json");
214+
res.end(JSON.stringify(result, null, 2));
215+
};
216+
217+
/** @type {import('express').Handler} */
218+
ServerState.getVersions = async function getVersions(req, res) {
219+
let result = {
220+
api: ServerState._appInfo.version,
221+
node: process.versions.node,
222+
icu: process.versions.icu,
223+
tz: process.versions.tz,
224+
};
225+
226+
res.setHeader("Content-Type", "application/json");
227+
res.end(JSON.stringify(result, null, 2));
228+
};
229+
230+
export default ServerState;
231+
export let init = ServerState.init;
232+
export let STARTED_AT = ServerState.STARTED_AT;
233+
export let getEnvs = ServerState.getEnvs;
234+
export let getPong = ServerState.getPong;
235+
export let getUptime = ServerState.getUptime;
236+
export let getVersion = ServerState.getVersion;
237+
export let getVersions = ServerState.getVersions;

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,12 @@
4444
"bugs": {
4545
"url": "https://github.com/beyondcodebootcamp/node-express-starter/issues"
4646
},
47-
"homepage": "https://github.com/beyondcodebootcamp/node-express-starter#readme"
47+
"homepage": "https://github.com/beyondcodebootcamp/node-express-starter#readme",
48+
"dependencies": {
49+
"@root/uuidv7": "^1.0.1",
50+
"dotenv": "^16.4.7",
51+
"express": "^5.0.1",
52+
"morgan": "^1.10.0",
53+
"xtz": "^1.3.2"
54+
}
4855
}

0 commit comments

Comments
 (0)