@ti-engine/web-framework 1.27.0 → 1.28.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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
4
4
 
5
+ ## Version 1.28.0
6
+
7
+ * feat(deploy): add `bin/healthcheck.js`, the container liveness probe for any `TiWebServer` application. Point a
8
+ Dockerfile `HEALTHCHECK` at `node /app/node_modules/@ti-engine/web-framework/bin/healthcheck.js`. It belongs here
9
+ rather than in each application because every input it reads is the framework's: `TI_WEB_USE_TLS`, `TI_WEB_PORT`
10
+ and `TI_WEB_TLS_CERT_PATH` are the framework's environment overrides, and `/health` is the framework's own route,
11
+ served by `webHandlers.healthHandler`. The framework has always provided the endpoint and never anything to call
12
+ it, which left every consumer writing an inline `node -e` — and the obvious inline version hardcodes `http://`,
13
+ so it reports a TLS-enabled container unhealthy forever and Docker restarts a server that is answering correctly.
14
+ The transport comes from the same `tools.toBool` the server uses, so the two cannot drift. With TLS on the
15
+ certificate is verified rather than skipped: trust is anchored to the server's own certificate and the name to
16
+ check is read out of it, so one issued for a public hostname passes while the probe connects to `127.0.0.1`.
17
+ Without a configured certificate the probe falls back to establishing that the port accepts connections — weaker,
18
+ but it neither disables verification nor restarts a healthy container. Arrived in competence 3.34.0 and moved
19
+ here unchanged in behaviour.
20
+
5
21
  ## Version 1.27.0
6
22
 
7
23
  Session lifetime. Two defects that together threw a signed-in user out roughly ten minutes after sign-in, however
package/README.md CHANGED
@@ -17,6 +17,7 @@ The web server configuration (host, port, TLS, cookies, etc.) is normally provid
17
17
  * `TI_WEB_USE_TLS` (`true`/`false`) toggles in-app TLS. Set `false` when a reverse proxy / ingress terminates TLS.
18
18
  * `TI_WEB_TLS_CERT_PATH` / `TI_WEB_TLS_KEY_PATH` override the TLS certificate/key paths (only used when TLS is enabled).
19
19
  * `TI_WEB_COOKIE_SECRET` sets the session cookie signing secret. Set a stable, private value for durable sessions and multi-replica deployments (otherwise a random per-process value is used).
20
+ * `TI_WEB_TLS_CERT_PATH` is also read by the container liveness probe (below), which verifies against that certificate.
20
21
  * `TI_WEB_SESSION_IDLE_TIMEOUT` (whole minutes) sets how long a signed-in session survives **without activity**, overriding `cookies.maxAge`. The window is rolling: every response re-stamps the cookie, so a session ends only after that long with no request at all. Note that a user typing into a form makes no requests, so set this comfortably longer than the longest form a user fills in one sitting. Defaults to 480 (eight hours).
21
22
  * `TI_WEB_AUTH_METHODS` (comma-separated) **replaces** the enabled authentication methods (`auth.enabledMethods`), e.g. `openid-google` or `local,openid-google`.
22
23
  * `TI_WEB_AUTH_LOCAL_USERS_PATH` overrides the local user directory's file path (`auth.local.usersPath`), which backs `local` sign-in. An explicitly empty value means *no directory*, so every local sign-in is refused. See [Local (username/password) authentication](#local-usernamepassword-authentication).
@@ -129,3 +130,24 @@ mkcert localhost 127.0.0.1 ::1
129
130
  ## License
130
131
 
131
132
  Apache-2.0 © Boris Kostadinov. See [LICENSE](LICENSE).
133
+
134
+ ## Container liveness probe
135
+
136
+ `bin/healthcheck.js` asks a running server whether it is still serving and exits 0 only if it says yes. Point a
137
+ Dockerfile `HEALTHCHECK` at it:
138
+
139
+ ```dockerfile
140
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
141
+ CMD ["node", "/app/node_modules/@ti-engine/web-framework/bin/healthcheck.js"]
142
+ ```
143
+
144
+ It reads `TI_WEB_USE_TLS` through the same `tools.toBool` the server uses, so the probe's transport cannot drift
145
+ from the server's, and calls `/health` — the unprotected route `webHandlers.healthHandler` serves. Writing this
146
+ inline in a Dockerfile is the mistake it exists to prevent: an `http://` URL hardcoded there reports a TLS-enabled
147
+ container unhealthy forever, and Docker restarts a server that is answering correctly.
148
+
149
+ With TLS on it verifies the certificate rather than skipping verification, anchoring trust to the server's own
150
+ certificate at `TI_WEB_TLS_CERT_PATH` and taking the name to check from that certificate — so a certificate issued
151
+ for a public hostname still passes while the probe connects to `127.0.0.1`. Set that variable when you terminate
152
+ TLS inside the container. Without it there is nothing to anchor to and the probe falls back to establishing that
153
+ the port accepts connections, which is weaker but neither disables verification nor restarts a healthy container.
@@ -0,0 +1,136 @@
1
+ /*
2
+ * The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
3
+ * Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ /**
19
+ * Container liveness probe for any application built on {@link TiWebServer}: ask the server whether it is still
20
+ * serving, and exit 0 only if it says yes. Invoke it from a Dockerfile `HEALTHCHECK`, the way the competence image
21
+ * does:
22
+ * <br/>
23
+ * `HEALTHCHECK CMD ["node", "/app/node_modules/@ti-engine/web-framework/bin/healthcheck.js"]`
24
+ * <br/>
25
+ * It lives here rather than in an application because every input it reads is the framework's: `TI_WEB_USE_TLS`,
26
+ * `TI_WEB_PORT` and `TI_WEB_TLS_CERT_PATH` are the framework's environment overrides, `/health` is the framework's
27
+ * route, and the endpoint it calls is `webHandlers.healthHandler`. An application copying this file would be
28
+ * copying framework behaviour, and would silently keep the old behaviour when the framework's changed.
29
+ * <br/>
30
+ * The obvious implementation is an inline `node -e` in the Dockerfile, and the obvious mistake is what that
31
+ * encourages: hardcoding `require('http')` and an `http://` URL. That works for as long as TLS is off, and the
32
+ * moment an image is run with TLS on — supported, via a mounted certificate rather than terminating at a proxy —
33
+ * the probe speaks plain HTTP to a TLS listener, fails every time, and Docker reports a healthy container
34
+ * unhealthy, restarting it on a loop.
35
+ * <br/>
36
+ * The transport comes from the same variable and the same parser the server uses, so the two cannot drift:
37
+ * `tools.toBool` treats an unset value and `false`/`0`/`no`/`N` as false, and anything else as true.
38
+ * <br/>
39
+ * <b>On certificates.</b> The obvious way to make a loopback TLS probe work is `rejectUnauthorized: false`, and
40
+ * the obvious defence is that nothing can sit between a process and itself. Both are true and it is still the wrong
41
+ * line to write: it is the snippet that gets copied out of a health probe and into a client that does cross a
42
+ * network. So verification stays on and the trust anchor is narrowed instead — the server's own certificate, named
43
+ * by `TI_WEB_TLS_CERT_PATH`, is passed as the sole CA, and the name to verify is read out of that certificate
44
+ * rather than assumed to be `127.0.0.1` (a certificate issued for a public hostname is the normal case, and a probe
45
+ * that reported "dead" because of the name on it would be repeating the bug this file exists to fix).
46
+ * <br/>
47
+ * When no certificate path is configured there is nothing to anchor to, and the probe degrades to establishing that
48
+ * the port is accepting connections. That is weaker — it shows the listener is up, not that the application is
49
+ * answering — but it is honest, and it is the only remaining option that neither disables verification nor kills a
50
+ * container that is running perfectly well.
51
+ */
52
+
53
+ const fs = require( "node:fs" );
54
+ const net = require( "node:net" );
55
+ const tools = require( "@ti-engine/core/tools" );
56
+
57
+ const HOST = "127.0.0.1";
58
+ const TIMEOUT_MS = 4000; // below Docker's own --timeout, so a hung socket fails as unhealthy rather than being killed mid-probe
59
+ const port = process.env.TI_WEB_PORT || 3000;
60
+ const useTLS = tools.toBool( process.env.TI_WEB_USE_TLS );
61
+
62
+ const alive = () => process.exit( 0 );
63
+ const dead = () => process.exit( 1 );
64
+
65
+ /**
66
+ * Reads the server's own certificate and works out what to trust and what name to verify against.
67
+ *
68
+ * @returns {{ca: Buffer, servername: (string|undefined)}|null} null when no certificate is configured or readable.
69
+ */
70
+ function ownCertificate() {
71
+ const certPath = process.env.TI_WEB_TLS_CERT_PATH;
72
+ if ( !certPath ) {
73
+ return null;
74
+ }
75
+ try {
76
+ const pem = fs.readFileSync( certPath );
77
+ const certificate = new ( require( "node:crypto" ).X509Certificate )( pem );
78
+ // Connecting by IP verifies against an `IP Address:` entry, so a certificate that covers the loopback
79
+ // address needs no SNI at all. Otherwise the first DNS name it carries is the name it can satisfy; failing
80
+ // that, its common name.
81
+ const names = String( certificate.subjectAltName || "" ).split( "," ).map( ( entry ) => entry.trim() );
82
+ if ( names.includes( `IP Address:${ HOST }` ) ) {
83
+ return { ca: pem, servername: undefined };
84
+ }
85
+ const dnsName = names.find( ( entry ) => entry.startsWith( "DNS:" ) );
86
+ if ( dnsName ) {
87
+ return { ca: pem, servername: dnsName.slice( "DNS:".length ) };
88
+ }
89
+ const commonName = /CN=([^\n,]+)/.exec( String( certificate.subject || "" ) );
90
+ return { ca: pem, servername: commonName ? commonName[ 1 ].trim() : undefined };
91
+ } catch {
92
+ // Unreadable, or not a certificate. Nothing to anchor to.
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Last resort when TLS is on and no certificate is available to verify against: prove the port accepts connections.
99
+ *
100
+ * @returns {void}
101
+ */
102
+ function probeSocket() {
103
+ const socket = net.connect( { host: HOST, port: port } );
104
+ socket.setTimeout( TIMEOUT_MS );
105
+ socket.on( "connect", () => { socket.destroy(); alive(); } );
106
+ socket.on( "timeout", () => { socket.destroy(); dead(); } );
107
+ socket.on( "error", dead );
108
+ }
109
+
110
+ /**
111
+ * @param {Object} extraOptions Merged into the request options — the CA and server name for the TLS case.
112
+ * @returns {void}
113
+ */
114
+ function probeHealthEndpoint( extraOptions ) {
115
+ const transport = useTLS ? require( "node:https" ) : require( "node:http" );
116
+ const request = transport.get( { host: HOST, port: port, path: "/health", ...extraOptions }, ( response ) => {
117
+ response.resume();
118
+ process.exit( response.statusCode === 200 ? 0 : 1 );
119
+ } );
120
+ request.setTimeout( TIMEOUT_MS, () => {
121
+ request.destroy();
122
+ dead();
123
+ } );
124
+ request.on( "error", dead );
125
+ }
126
+
127
+ if ( !useTLS ) {
128
+ probeHealthEndpoint( {} );
129
+ } else {
130
+ const trust = ownCertificate();
131
+ if ( trust ) {
132
+ probeHealthEndpoint( { ca: trust.ca, servername: trust.servername } );
133
+ } else {
134
+ probeSocket();
135
+ }
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/web-framework",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "A web-framework based on the ti-engine. It provides a customizable ready-to-use web-server microservice and a set of tools for creating web applications. NOTICE: This is still a work in progress and the full architecture, design, and functionality are not available!",
5
5
  "keywords": [
6
6
  "ti-engine",
@@ -0,0 +1 @@
1
+ export {};