@ti-engine/web-framework 1.17.0 → 1.18.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.
package/CHANGELOG.md CHANGED
@@ -2,11 +2,35 @@
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.18.1
6
+
7
+ Enabling Azure SSO took the instance down at startup: the OAuth2 callback was registered by handing the configured callback value straight to Express as a route path, and the installation docs tell operators to set that value to the full absolute URL registered with the identity provider. Express 5 parses route patterns with path-to-regexp v8, where `:` opens a parameter name — so `https://host/login/azure-callback` throws `Missing parameter name at index 6` and the web server never starts. Google was affected identically, which also made this a prerequisite for the competence Cloud Run deployment, whose `deploy.sh` patches in an absolute callback URL (CA-97).
8
+
9
+ * fix(web-framework): register an OAuth2 callback by its **path** rather than by the configured value verbatim, so a callback given as the absolute URL registered with the provider no longer crashes startup; a callback that yields no usable path now logs a WARNING and skips that provider's endpoint instead of taking the instance down, matching how an enabled-but-unconfigured provider is already handled
10
+ * feat(web-framework): add `AuthManager.getOAuth2CallbackPath( authMethod )` and the pure, unit-tested `AuthManager.toCallbackPath( callbackUrl )` — reduces an absolute, protocol-relative, path or bare relative callback to its route path (query string and fragment stripped), or `null` when no usable path can be derived. The `redirect_uri` sent to the provider is deliberately left as configured, so an absolute callback keeps matching the provider registration exactly instead of depending on the forwarded protocol/host being correct
11
+ * docs(web-framework): document the OpenID Connect provider variables in the README, and state in the README, the competence `INSTALL.md` and `.env.example` that a callback URL may be given as either the absolute registered URL or a path — including what each implies for the `redirect_uri`, and that the path must be the one the app actually receives when a proxy strips a prefix
12
+ * build(release): bump package version from `1.18.0` to `1.18.1`
13
+
14
+ ## Version 1.18.0
15
+
16
+ Every web-server setting a container deployment needs could be supplied per environment except one: the admin allowlist. `auth.admins` was readable only from the config file baked into the image, so a containerized deployment had no way to name an administrator — leaving the admin configuration screens unreachable, or forcing a real identity to be committed to the repository. This closes that gap in the existing `TI_WEB_*` override set (CA-94).
17
+
18
+ * feat(web-framework): add the `TI_WEB_AUTH_ADMINS` environment override — comma-separated, **replaces** `auth.admins` (matched against the session user's user ID, username or email), so the admin allowlist is configurable per environment like every other web setting; an explicitly empty value means no admins
19
+ * docs(web-framework): document `TI_WEB_AUTH_METHODS` and `TI_WEB_TRUSTED_ORIGINS` in the README's environment-variable list, which had never listed them
20
+ * build(release): bump package version from `1.17.1` to `1.18.0`
21
+
22
+ ## Version 1.17.1
23
+
24
+ A validator that needs to compare its own config document against its previously committed state had no way to do so: `applyEdits`'s cross-document context resolves `getConfig` to the *pending* value for any document inside the current edit batch — by design, so a validator can check a sibling document's post-edit state — but a document is always part of its own edit batch, so calling `getConfig` on itself just hands back the same incoming value already passed as the validator's argument, never its prior state. This silently defeated the competence `research-consent` config's version-bump guard (CA-93).
25
+
26
+ * feat(web-framework): add `getStoredConfig(key)` to the `applyEdits` validator context (`ConfigService`) — always resolves the current *committed* value from the store, even for the document currently under validation, so a validator comparing its own document against its previous state has a way to do it; purely additive — `getConfig`'s existing cross-document (pending-value) semantics are unchanged
27
+ * build(release): bump package version from `1.17.0` to `1.17.1`
28
+
5
29
  ## Version 1.17.0
6
30
 
7
31
  Route-registration seams so an application subclass can add its own Express routes and unprotected-route patterns — enabling public, content-driven sites (the first consumer being the standalone author's site) to layer a catch-all content resolver over the framework without reaching into private state.
8
32
 
9
- * feat(web-server): add `TiWebServer.registerRoute( method, path, ...handlers )` — registers a custom route on the underlying Express app from a `defineWebApplicationRoutes()` override (after `super()`), so a catch-all resolver can be mounted after the framework's own routes but before its `*splat` 404 handler. Limited to route-scoped verbs (get/post/put/patch/delete/options/head/all); raises `E_GEN_INVALID_ARGUMENT_TYPE` for any other method and `E_GEN_NOT_INITIALIZED` if called before the Express app exists
33
+ * feat(web-server): add `TiWebServer.registerRoute( method, path, ...handlers )` — registers a custom route on the underlying Express app from a `defineWebApplicationRoutes()` override (after `super()`), so a catch-all resolver can be mounted after the framework's own routes but before its `*splat` 404 handler. Limited to route-scoped verbs (get/post/put/patch/delete/options/head/all); raises `E_GEN_INVALID_ARGUMENT_TYPE` for any other method and `E_GEN_NOT_INITIALIZED` if called before the Express app exists. The method must be a **string** — a non-string is rejected outright rather than coerced, so a value whose `toString()` happens to yield a verb (`[ "get" ]`, `new String( "get" )`) cannot slip past the allowlist and register a route
10
34
  * feat(web-server): add `TiWebServer.addUnprotectedRoute( pattern )` — appends a string (exact-match) or RegExp (tested) pattern to the unprotected-routes list from a `defineUnprotectedRoutes()` override, so a public-by-default site can invert the framework's protect-by-default stance; non-string/non-RegExp values are ignored with a warning
11
35
  * refactor(web-server): extract the unprotected-route matching loop from `isUnprotectedRoute()` into the pure, unit-tested `isRouteInList()` helper (behavior unchanged, including the defensive `lastIndex` reset), and add the `normalizeRegistrableMethod()` helper — both exported for testing alongside the existing `RE_*` matcher constants
12
36
  * build(release): bump package version from `1.16.0` to `1.17.0`
package/README.md CHANGED
@@ -17,6 +17,11 @@ 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_AUTH_METHODS` (comma-separated) **replaces** the enabled authentication methods (`auth.enabledMethods`), e.g. `openid-google` or `local,openid-google`.
21
+ * `TI_WEB_AUTH_ADMINS` (comma-separated) **replaces** the admin allowlist (`auth.admins`). Entries are matched against the session user's user ID, username or email, so an OpenID deployment lists emails. An explicitly empty value means *no admins*.
22
+ * `TI_WEB_TRUSTED_ORIGINS` (comma-separated) **replaces** the trusted request origins (`trustedOrigins`) — needed behind proxies that do not present the real external origin.
23
+
24
+ OpenID Connect providers are configured with their own variables — `TI_AZURE_AUTH_CLIENT_ID` / `TI_AZURE_AUTH_CLIENT_SECRET` / `TI_AZURE_AUTH_CALLBACK_URL` / `TI_AZURE_AUTH_DISCOVERY_URL`, and the `TI_GCLOUD_AUTH_*` equivalents. A callback URL may be given either as the full absolute URL registered with the provider (`https://your-host/login/azure-callback`) or as a path (`/login/azure-callback`): the server always listens on the path, while the `redirect_uri` sent to the provider is the absolute value verbatim if one was configured, and otherwise assembled from the request's forwarded protocol/host.
20
25
 
21
26
  ## Configure HTTPS for development
22
27
 
@@ -1,37 +1,37 @@
1
- <div x-id="['component-flyout']"
2
- x-data="tiComponentSidebarFlyout('{ti-config-key}')"
3
- class="ti-sidebar-flyout-container">
4
- <!--
5
- Trigger slot. The placeholder consumer can replace this with any custom button — typically a user avatar
6
- or app menu trigger. The trigger MUST set x-ref="flyoutButton" and call toggle() to integrate with the
7
- encapsulated flyout state. If the consumer omits inner content, this default icon button is used.
8
- -->
9
- <ti-slot>
10
- <button x-on:click.stop="toggle()"
11
- x-on:ti-close-all-flyout="close()"
12
- x-bind:aria-expanded="isOpen"
13
- x-bind:aria-controls="$id('component-flyout')"
14
- x-bind:aria-label="menuTitle"
15
- x-ref="flyoutButton"
16
- type="button" class="ti-sidebar-button">
17
- <span class="ti-icon" x-bind:class="icon" aria-hidden="true"></span>
18
- </button>
19
- </ti-slot>
20
- <div x-on:click.outside="close()"
21
- x-bind:id="$id('component-flyout')"
22
- x-show="isOpen"
23
- x-ref="flyoutPanel"
24
- role="menu" class="ti-sidebar-flyout">
25
- <template x-for="(buttonConfig, index) in buttonConfigs">
26
- <button x-on:click="close()"
27
- x-bind:hx-get="(!buttonConfig.action.method || buttonConfig.action.method === 'get') ? buttonConfig.action.href : null"
28
- x-bind:hx-post="(buttonConfig.action.method && buttonConfig.action.method === 'post') ? buttonConfig.action.href : null"
29
- x-bind:hx-target="buttonConfig.action.target"
30
- x-bind:hx-swap="buttonConfig.action.swap"
31
- hx-push-url="true"
32
- type="button" class="ti-sidebar-flyout-item">
33
- <span class="ti-icon" x-bind:class="buttonConfig.icon" aria-hidden="true"></span><span x-text="buttonConfig.title"></span>
34
- </button>
35
- </template>
36
- </div>
1
+ <div x-id="['component-flyout']"
2
+ x-data="tiComponentSidebarFlyout('{ti-config-key}')"
3
+ class="ti-sidebar-flyout-container">
4
+ <!--
5
+ Trigger slot. The placeholder consumer can replace this with any custom button — typically a user avatar
6
+ or app menu trigger. The trigger MUST set x-ref="flyoutButton" and call toggle() to integrate with the
7
+ encapsulated flyout state. If the consumer omits inner content, this default icon button is used.
8
+ -->
9
+ <ti-slot>
10
+ <button x-on:click.stop="toggle()"
11
+ x-on:ti-close-all-flyout="close()"
12
+ x-bind:aria-expanded="isOpen"
13
+ x-bind:aria-controls="$id('component-flyout')"
14
+ x-bind:aria-label="menuTitle"
15
+ x-ref="flyoutButton"
16
+ type="button" class="ti-sidebar-button">
17
+ <span class="ti-icon" x-bind:class="icon" aria-hidden="true"></span>
18
+ </button>
19
+ </ti-slot>
20
+ <div x-on:click.outside="close()"
21
+ x-bind:id="$id('component-flyout')"
22
+ x-show="isOpen"
23
+ x-ref="flyoutPanel"
24
+ role="menu" class="ti-sidebar-flyout">
25
+ <template x-for="(buttonConfig, index) in buttonConfigs">
26
+ <button x-on:click="close()"
27
+ x-bind:hx-get="(!buttonConfig.action.method || buttonConfig.action.method === 'get') ? buttonConfig.action.href : null"
28
+ x-bind:hx-post="(buttonConfig.action.method && buttonConfig.action.method === 'post') ? buttonConfig.action.href : null"
29
+ x-bind:hx-target="buttonConfig.action.target"
30
+ x-bind:hx-swap="buttonConfig.action.swap"
31
+ hx-push-url="true"
32
+ type="button" class="ti-sidebar-flyout-item">
33
+ <span class="ti-icon" x-bind:class="buttonConfig.icon" aria-hidden="true"></span><span x-text="buttonConfig.title"></span>
34
+ </button>
35
+ </template>
36
+ </div>
37
37
  </div>
package/bin/web-server.js CHANGED
@@ -516,12 +516,18 @@ class TiWebServer extends ServiceConsumer {
516
516
  this.#webServer.post( "/logout", webHandlers.logoutHandler() );
517
517
  this.#webServer.get( "/health", webHandlers.healthHandler() );
518
518
  this.#webServer.get( "/me", webHandlers.userInformationHandler() );
519
- if ( this.#authManager.isAuthEnabled( authMethod.OPENID_GOOGLE ) ) {
520
- this.#webServer.get( this.#authManager.getOAuth2CallbackUrl( authMethod.OPENID_GOOGLE ), webHandlers.authorizedOAuth2CallbackHandler( this, authMethod.OPENID_GOOGLE ) );
521
- }
522
- if ( this.#authManager.isAuthEnabled( authMethod.OPENID_AZURE ) ) {
523
- this.#webServer.get( this.#authManager.getOAuth2CallbackUrl( authMethod.OPENID_AZURE ), webHandlers.authorizedOAuth2CallbackHandler( this, authMethod.OPENID_AZURE ) );
524
- }
519
+ // NOTE: A callback is registered by its path, never by the configured value verbatim — that value is commonly
520
+ // the absolute URL registered with the identity provider, which Express cannot parse as a route pattern.
521
+ [ authMethod.OPENID_GOOGLE, authMethod.OPENID_AZURE ].forEach( ( method ) => {
522
+ if ( this.#authManager.isAuthEnabled( method ) === true ) {
523
+ const callbackPath = this.#authManager.getOAuth2CallbackPath( method );
524
+ if ( callbackPath ) {
525
+ this.#webServer.get( callbackPath, webHandlers.authorizedOAuth2CallbackHandler( this, method ) );
526
+ } else {
527
+ logger.log( `Authentication method '${ method }' is enabled but its callback URL yields no usable route path; its callback endpoint was not registered and sign-in through it will fail.`, logger.logSeverity.WARNING );
528
+ }
529
+ }
530
+ } );
525
531
 
526
532
  // Admin configuration-management API. Gated by the admin role; these paths are not in the unprotected-routes
527
533
  // list, so they also inherit the server's global authentication + CSRF middleware.
@@ -623,7 +629,10 @@ class TiWebServer extends ServiceConsumer {
623
629
 
624
630
  /**
625
631
  * Normalizes an HTTP method to a lower-case Express routing verb, or returns null if it is not a supported,
626
- * registrable verb. Pure and static; exposed for unit testing not part of the customization surface.
632
+ * registrable verb. Anything that is not a string is rejected outright rather than coerced otherwise a value
633
+ * whose `toString()` happens to yield a verb (`[ "get" ]`, `new String( "get" )`) would register a route and
634
+ * bypass the `E_GEN_INVALID_ARGUMENT_TYPE` that {@link TiWebServer#registerRoute} raises for a bad method.
635
+ * Pure and static; exposed for unit testing — not part of the customization surface.
627
636
  *
628
637
  * @method
629
638
  * @static
@@ -632,7 +641,10 @@ class TiWebServer extends ServiceConsumer {
632
641
  * @public
633
642
  */
634
643
  static normalizeRegistrableMethod( method ) {
635
- const verb = String( method || "" ).trim().toLowerCase();
644
+ if ( typeof method !== "string" ) {
645
+ return null;
646
+ }
647
+ const verb = method.trim().toLowerCase();
636
648
  return TiWebServer.#REGISTRABLE_METHODS.has( verb ) ? verb : null;
637
649
  }
638
650
 
@@ -226,6 +226,51 @@ class AuthManager {
226
226
  }
227
227
  }
228
228
 
229
+ /**
230
+ * Used to get the local route path of the callback for the specified OAuth2 authentication method.
231
+ * <br/>
232
+ * A callback can legitimately be configured either as a path or as the full absolute URL registered with the
233
+ * identity provider. The absolute form is what the provider expects as the redirect URI, but it is not a usable
234
+ * Express route pattern, so this reduces whatever is configured to the path the server must actually listen on.
235
+ *
236
+ * @method
237
+ * @param {TiAuthMethod} authMethod
238
+ * @returns {string|null} The route path, or null if the configured callback yields no usable path.
239
+ * @throws {TiException.E_SEC_UNRECOGNIZED_AUTH_METHOD} If the requested OAuth2 method is not recognized or enabled.
240
+ * @public
241
+ */
242
+ getOAuth2CallbackPath( authMethod ) {
243
+ return AuthManager.toCallbackPath( this.getOAuth2CallbackUrl( authMethod ) );
244
+ }
245
+
246
+ /**
247
+ * Reduces a configured OAuth2 callback value to the local route path it corresponds to. Accepts an absolute URL
248
+ * ('https://host/login/azure-callback'), a protocol-relative URL, or a path with or without its leading slash,
249
+ * and strips any query string or fragment. Pure and static; exposed for unit testing.
250
+ * <br/>
251
+ * NOTE: This exists because Express 5 parses a route pattern with path-to-regexp v8, where ':' opens a parameter
252
+ * name — so an absolute URL used verbatim as a route path throws 'Missing parameter name' at startup.
253
+ *
254
+ * @method
255
+ * @static
256
+ * @param {string} callbackUrl
257
+ * @returns {string|null} The route path, or null if no usable path can be derived.
258
+ * @public
259
+ */
260
+ static toCallbackPath( callbackUrl ) {
261
+ const value = String( callbackUrl || "" ).trim();
262
+ if ( value === "" ) {
263
+ return null;
264
+ }
265
+ try {
266
+ // The base is only a parsing anchor — an absolute or protocol-relative value overrides it, while a
267
+ // path or bare relative value resolves against it. Either way only the pathname is used.
268
+ return new URL( value, "http://localhost" ).pathname;
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
229
274
  /* Private interface */
230
275
 
231
276
  /**
@@ -41,9 +41,23 @@ const instancePathToDataPath = ( instancePath ) => {
41
41
  */
42
42
 
43
43
  /**
44
- * @typedef {function(Object, Object): (ConfigValidationIssue[]|Promise<ConfigValidationIssue[]>)} SemanticValidator
45
- * A semantic validator receives the candidate value and a context object (e.g. `{ getConfig(key) }` to read other
46
- * current configs for cross-document checks) and returns the issues it found (empty array = OK). May be async.
44
+ * The cross-document read context passed to every {@link SemanticValidator}, built fresh for each
45
+ * {@link ConfigService#applyEdits} call.
46
+ *
47
+ * @typedef {Object} ValidatorContext
48
+ * @property {function(string): Promise<*>} getConfig Resolves the *pending* value of `key` when it is part of the
49
+ * current edit batch, otherwise its current committed value. Lets a validator check a sibling document's
50
+ * post-edit state — but calling this for the document being validated itself just returns the same incoming
51
+ * value already passed as the validator's first argument, not its prior state.
52
+ * @property {function(string): Promise<*>} getStoredConfig Always resolves the current committed value of `key`,
53
+ * even when `key` is the document currently under validation. Use this to compare a document against its own
54
+ * previous state (e.g. detecting an edit that should have bumped a version marker).
55
+ */
56
+
57
+ /**
58
+ * @typedef {function(Object, ValidatorContext): (ConfigValidationIssue[]|Promise<ConfigValidationIssue[]>)} SemanticValidator
59
+ * A semantic validator receives the candidate value and a {@link ValidatorContext} and returns the issues it found
60
+ * (empty array = OK). May be async.
47
61
  */
48
62
 
49
63
  /**
@@ -163,7 +177,7 @@ class ConfigRegistry {
163
177
  * @method
164
178
  * @param {string} configKey
165
179
  * @param {Object} value
166
- * @param {Object} [context] Passed to each semantic validator (e.g. `{ getConfig(key) }`).
180
+ * @param {ValidatorContext} [context] Passed to each semantic validator.
167
181
  * @returns {Promise<{valid: boolean, errors: ConfigValidationIssue[]}>}
168
182
  * @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
169
183
  * @public
@@ -13,9 +13,11 @@ const exceptions = require( "@ti-engine/core/exceptions" );
13
13
  *
14
14
  * Two layers:
15
15
  * - **Document level** — {@link ConfigService#applyEdits}: validate every affected document (schema + semantic,
16
- * with a cross-document context that sees the *pending* values of the same edit) and, only if all pass, commit
17
- * them as one change-set. Validation failures return `{ ok:false, errors }` and write nothing; a version
18
- * conflict from the store surfaces as a rejection.
16
+ * with a cross-document {@link ValidatorContext} whose `getConfig` sees the *pending* values of the same edit
17
+ * letting a validator check a sibling document's post-edit state while `getStoredConfig` always returns the
18
+ * committed value, even for the document currently under validation) and, only if all pass, commit them as one
19
+ * change-set. Validation failures return `{ ok:false, errors }` and write nothing; a version conflict from the
20
+ * store surfaces as a rejection.
19
21
  * - **Entity level** — composite editors registered with `compose(docs)→view` / `decompose(edited, docs)→{key:value}`,
20
22
  * so the UI edits a domain entity (e.g. a "competency") that is projected from, and scattered back into, several
21
23
  * documents. {@link ConfigService#saveEditorEdit} decomposes the edit and routes it through `applyEdits`.
@@ -61,8 +63,13 @@ class ConfigService {
61
63
  return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-apply-input" } ) );
62
64
  }
63
65
 
64
- // Cross-document validation context: a document being edited is seen at its *pending* value; others are read
65
- // from the store. This lets a validator on one document check against the post-edit state of its siblings.
66
+ // Cross-document validation context: a document being edited is seen at its *pending* value via getConfig
67
+ // even when that document is the one currently under validation, so calling getConfig on "yourself" just
68
+ // hands back the same incoming value already passed as the validator's first argument, not its prior state.
69
+ // This lets a validator on one document check against the post-edit state of its siblings. getStoredConfig
70
+ // is the counterpart: it always resolves the committed value, so a validator that must compare its own
71
+ // document against its previous state (e.g. detecting an edit that should have bumped a version marker)
72
+ // uses that instead.
66
73
  const pending = {};
67
74
  for ( const edit of edits ) {
68
75
  pending[ edit.configKey ] = edit.value;
@@ -73,7 +80,11 @@ class ConfigService {
73
80
  return Promise.resolve( clone( pending[ key ] ) );
74
81
  }
75
82
  return this.#store.getCurrent( key ).then( ( current ) => ( current ? current.value : null ) );
76
- }
83
+ },
84
+ // Always the committed value, even for a document inside this edit batch. A validator comparing its own
85
+ // document against its previous state must use this; getConfig would hand back the pending value it is
86
+ // currently validating.
87
+ getStoredConfig: ( key ) => this.#store.getCurrent( key ).then( ( current ) => ( current ? current.value : null ) )
77
88
  };
78
89
 
79
90
  return Promise.all( edits.map( ( edit ) => {
@@ -1,64 +1,68 @@
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
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
- * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
- */
8
-
9
- "use strict";
10
-
11
- const tools = require( "@ti-engine/core/tools" );
12
-
13
- /**
14
- * Applies TI_WEB_* environment-variable overrides onto an (already-merged) web server configuration object.
15
- * Each override is applied ONLY when its environment variable is defined, so an absent variable leaves the
16
- * configured/default value untouched (fully backward compatible). This gives ti-engine web servers 12-factor,
17
- * container-friendly control over network binding, TLS, the session cookie secret, the enabled authentication
18
- * methods, and the trusted request origins without editing config files. Note `TI_WEB_AUTH_METHODS` and
19
- * `TI_WEB_TRUSTED_ORIGINS` fully REPLACE their config arrays (`auth.enabledMethods` / `trustedOrigins`) rather than
20
- * merging — the config-file merge is by-index and cannot cleanly override an array.
21
- *
22
- * @method
23
- * @param {Object} config The web server configuration to augment (mutated in place and returned).
24
- * @param {Object} [env=process.env] The environment source (injectable for testing).
25
- * @returns {Object} The same config object, with any present overrides applied.
26
- * @public
27
- */
28
- function applyWebConfigEnvOverrides( config, env = process.env ) {
29
- if ( !config || typeof config !== "object" ) {
30
- return config;
31
- }
32
- if ( env.TI_WEB_HOST !== undefined ) {
33
- config.host = env.TI_WEB_HOST;
34
- }
35
- if ( env.TI_WEB_PORT !== undefined ) {
36
- const port = Number( env.TI_WEB_PORT );
37
- if ( Number.isInteger( port ) ) {
38
- config.port = port;
39
- }
40
- }
41
- if ( env.TI_WEB_USE_TLS !== undefined ) {
42
- config.useTLS = tools.toBool( env.TI_WEB_USE_TLS );
43
- }
44
- if ( env.TI_WEB_TLS_CERT_PATH !== undefined ) {
45
- config.tlsCertPath = env.TI_WEB_TLS_CERT_PATH;
46
- }
47
- if ( env.TI_WEB_TLS_KEY_PATH !== undefined ) {
48
- config.tlsKeyPath = env.TI_WEB_TLS_KEY_PATH;
49
- }
50
- if ( env.TI_WEB_COOKIE_SECRET !== undefined ) {
51
- config.cookies = config.cookies || {};
52
- config.cookies.secret = env.TI_WEB_COOKIE_SECRET;
53
- }
54
- if ( env.TI_WEB_AUTH_METHODS !== undefined ) {
55
- config.auth = config.auth || {};
56
- config.auth.enabledMethods = env.TI_WEB_AUTH_METHODS.split( "," ).map( ( method ) => method.trim() ).filter( ( method ) => method.length > 0 );
57
- }
58
- if ( env.TI_WEB_TRUSTED_ORIGINS !== undefined ) {
59
- config.trustedOrigins = env.TI_WEB_TRUSTED_ORIGINS.split( "," ).map( ( origin ) => origin.trim() ).filter( ( origin ) => origin.length > 0 );
60
- }
61
- return config;
62
- }
63
-
64
- module.exports = applyWebConfigEnvOverrides;
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
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
+ * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const tools = require( "@ti-engine/core/tools" );
12
+
13
+ /**
14
+ * Applies TI_WEB_* environment-variable overrides onto an (already-merged) web server configuration object.
15
+ * Each override is applied ONLY when its environment variable is defined, so an absent variable leaves the
16
+ * configured/default value untouched (fully backward compatible). This gives ti-engine web servers 12-factor,
17
+ * container-friendly control over network binding, TLS, the session cookie secret, the enabled authentication
18
+ * methods, the admin allowlist, and the trusted request origins without editing config files. Note `TI_WEB_AUTH_METHODS`,
19
+ * `TI_WEB_AUTH_ADMINS`, and `TI_WEB_TRUSTED_ORIGINS` fully REPLACE their config arrays (`auth.enabledMethods` / `auth.admins` / `trustedOrigins`) rather than
20
+ * merging — the config-file merge is by-index and cannot cleanly override an array.
21
+ *
22
+ * @method
23
+ * @param {Object} config The web server configuration to augment (mutated in place and returned).
24
+ * @param {Object} [env=process.env] The environment source (injectable for testing).
25
+ * @returns {Object} The same config object, with any present overrides applied.
26
+ * @public
27
+ */
28
+ function applyWebConfigEnvOverrides( config, env = process.env ) {
29
+ if ( !config || typeof config !== "object" ) {
30
+ return config;
31
+ }
32
+ if ( env.TI_WEB_HOST !== undefined ) {
33
+ config.host = env.TI_WEB_HOST;
34
+ }
35
+ if ( env.TI_WEB_PORT !== undefined ) {
36
+ const port = Number( env.TI_WEB_PORT );
37
+ if ( Number.isInteger( port ) ) {
38
+ config.port = port;
39
+ }
40
+ }
41
+ if ( env.TI_WEB_USE_TLS !== undefined ) {
42
+ config.useTLS = tools.toBool( env.TI_WEB_USE_TLS );
43
+ }
44
+ if ( env.TI_WEB_TLS_CERT_PATH !== undefined ) {
45
+ config.tlsCertPath = env.TI_WEB_TLS_CERT_PATH;
46
+ }
47
+ if ( env.TI_WEB_TLS_KEY_PATH !== undefined ) {
48
+ config.tlsKeyPath = env.TI_WEB_TLS_KEY_PATH;
49
+ }
50
+ if ( env.TI_WEB_COOKIE_SECRET !== undefined ) {
51
+ config.cookies = config.cookies || {};
52
+ config.cookies.secret = env.TI_WEB_COOKIE_SECRET;
53
+ }
54
+ if ( env.TI_WEB_AUTH_METHODS !== undefined ) {
55
+ config.auth = config.auth || {};
56
+ config.auth.enabledMethods = env.TI_WEB_AUTH_METHODS.split( "," ).map( ( method ) => method.trim() ).filter( ( method ) => method.length > 0 );
57
+ }
58
+ if ( env.TI_WEB_AUTH_ADMINS !== undefined ) {
59
+ config.auth = config.auth || {};
60
+ config.auth.admins = env.TI_WEB_AUTH_ADMINS.split( "," ).map( ( entry ) => entry.trim() ).filter( ( entry ) => entry.length > 0 );
61
+ }
62
+ if ( env.TI_WEB_TRUSTED_ORIGINS !== undefined ) {
63
+ config.trustedOrigins = env.TI_WEB_TRUSTED_ORIGINS.split( "," ).map( ( origin ) => origin.trim() ).filter( ( origin ) => origin.length > 0 );
64
+ }
65
+ return config;
66
+ }
67
+
68
+ module.exports = applyWebConfigEnvOverrides;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/web-framework",
3
- "version": "1.17.0",
3
+ "version": "1.18.1",
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
  "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
6
  "license": "GPL-3.0-or-later",