@ti-engine/web-framework 1.20.1 → 1.23.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.
@@ -13,12 +13,19 @@ export type ApiConfig = {
13
13
  };
14
14
  export type SettingsAuth = {
15
15
  enabledMethods: string[];
16
- local: Object;
16
+ local: SettingsAuthLocal;
17
17
  oauth2: {
18
18
  azure?: SettingsOAuth2Client;
19
19
  google?: SettingsOAuth2Client;
20
20
  };
21
21
  };
22
+ export type SettingsAuthLocal = {
23
+ /**
24
+ * Path to the JSON file of local user records (see `TI_WEB_AUTH_LOCAL_USERS_PATH`).
25
+ * Local sign-in refuses everyone whenever this is absent, unreadable, or yields no usable records.
26
+ */
27
+ usersPath?: string;
28
+ };
22
29
  export type SettingsOAuth2Client = {
23
30
  clientID?: string;
24
31
  clientSecret?: string;
@@ -179,6 +186,11 @@ declare class TiWebServer extends ServiceConsumer {
179
186
  * Hook for the application to augment the freshly-authenticated session (e.g. derive domain roles from an
180
187
  * identity store or the org chart). Runs synchronously, once per login, before the framework's additive `admin`
181
188
  * role is applied. The default is a no-op. Any test-user role injection is an override of whatever the app derives.
189
+ * <br/>
190
+ * **Refusing a login.** Throwing from this hook refuses the sign-in: the framework destroys the freshly regenerated
191
+ * session (so no usable session survives the refusal), the login handler raises `401`, and the error handler
192
+ * redirects the browser to the login page with the exception code in `?error=`. Throw when the authenticated
193
+ * identity cannot be mapped to an application principal; return the session unchanged to accept it.
182
194
  *
183
195
  * @method
184
196
  * @virtual
@@ -200,6 +212,12 @@ declare class TiWebServer extends ServiceConsumer {
200
212
  authenticate(authMethod: TiAuthMethod, authDetails?: Object): Promise<any>;
201
213
  /**
202
214
  * Used to set up user authorization according to the specified auth method.
215
+ * <br/>
216
+ * NOTE: This presupposes a successful, immediately preceding {@link TiWebServer#authenticate} call for the same
217
+ * credentials — it is **not** an independent authentication check. For the `local` method it builds the session
218
+ * user from the directory record named by `oidc.username`, verifying that the record exists and is not disabled
219
+ * but performing no password comparison of its own; the framework's own login route calls `authenticate` first.
220
+ * Calling this directly without that preceding step would mint a session for any known username.
203
221
  *
204
222
  * @method
205
223
  * @param {TiAuthMethod} authMethod
@@ -0,0 +1,53 @@
1
+ declare const _exports: {
2
+ buildApplicationInfo: typeof buildApplicationInfo;
3
+ readApplicationManifest: typeof readApplicationManifest;
4
+ };
5
+ export = _exports;
6
+ import type { TiApplicationInfo } from "#definitions";
7
+ /**
8
+ * Builds the normalized application-information descriptor that backs the framework "About" screen.
9
+ * <br/>
10
+ * The function is PURE — everything it needs is injected — so the whole resolution order (manifest → environment
11
+ * override) is unit-testable without touching the filesystem or `process.env`. The impure half, reading the
12
+ * consuming application's manifest, is {@link readApplicationManifest}.
13
+ * <br/>
14
+ * Resolution order for the three overridable fields is manifest first, environment last:
15
+ * - `TI_WEB_APP_NAME` overrides `manifest.displayName` / a display name derived from `manifest.name`;
16
+ * - `TI_WEB_APP_VERSION` overrides `manifest.version`;
17
+ * - `TI_WEB_APP_RELEASE_DATE` overrides `manifest.releaseDate`.
18
+ * <br/>
19
+ * The environment wins because it is how a container image stamps facts that its baked-in manifest cannot know —
20
+ * most importantly the build/release date, for which `package.json` has no standard field at all.
21
+ *
22
+ * @method
23
+ * @param {Object} [options]
24
+ * @param {Object} [options.manifest] A `package.json`-shaped object for the consuming application.
25
+ * @param {Object} [options.env] The environment source (injectable for testing).
26
+ * @param {Array<{name: string, version: string}>} [options.components] Framework component versions to list.
27
+ * @param {Object} [options.runtime] Runtime facts (node/platform/instance). Included verbatim when present; the
28
+ * caller decides whether the current session is allowed to see them.
29
+ * @returns {TiApplicationInfo}
30
+ * @public
31
+ */
32
+ declare function buildApplicationInfo(options?: {
33
+ manifest?: Object;
34
+ env?: Object;
35
+ components?: Array<{
36
+ name: string;
37
+ version: string;
38
+ }>;
39
+ runtime?: Object;
40
+ }): TiApplicationInfo;
41
+ /**
42
+ * Reads the consuming application's `package.json`. This is the one impure function in this module.
43
+ * <br/>
44
+ * NOTE: A missing or malformed manifest resolves to an empty object rather than throwing — an informational screen
45
+ * must never be the reason a request fails, and {@link buildApplicationInfo} produces a usable (if sparse)
46
+ * descriptor from `{}`.
47
+ *
48
+ * @method
49
+ * @param {string} [directory=process.cwd()] The directory holding the manifest.
50
+ * @returns {Object}
51
+ * @public
52
+ */
53
+ declare function readApplicationManifest(directory?: string): Object;
@@ -70,6 +70,13 @@ declare class AuthManager {
70
70
  authenticate(authMethod: TiAuthMethod, authDetails: Object): Promise<Object>;
71
71
  /**
72
72
  * Used to set up user authorization according to the specified authentication method.
73
+ * <br/>
74
+ * NOTE: This presupposes a successful, immediately preceding {@link AuthManager#authenticate} call for the
75
+ * same credentials and is NOT an independent authentication check on its own — for `LOCAL` it performs no
76
+ * password verification. It refuses an absent, disabled, or (for `LOCAL`) not-yet-usable-directory record,
77
+ * but a caller that invokes it without having just authenticated bypasses password verification entirely.
78
+ * The framework's own login route always calls `authenticate()` first (see `web-handlers.js`); this method
79
+ * is public on both `AuthManager` and `TiWebServer`, so any other caller must preserve that ordering itself.
73
80
  *
74
81
  * @method
75
82
  * @param {TiAuthMethod} authMethod
@@ -11,6 +11,112 @@ export type TiSession = {
11
11
  destroy: (callback: TiSessionCallback) => TiSession;
12
12
  save: (callback?: TiSessionCallback) => TiSession;
13
13
  };
14
+ export type TiInfoItem = {
15
+ label: string;
16
+ value?: string;
17
+ /**
18
+ * Renders the value as a link to this target. Only `http:`, `https:` and `mailto:` are
19
+ * honoured — any other scheme is dropped client-side and the item degrades to plain text.
20
+ */
21
+ href?: string;
22
+ /**
23
+ * Span the full width of the section grid instead of one column.
24
+ */
25
+ wide?: boolean;
26
+ /**
27
+ * Render the value in the monospaced face (IDs, versions, hashes).
28
+ */
29
+ mono?: boolean;
30
+ /**
31
+ * Render the value as a dimmed hint rather than primary text.
32
+ */
33
+ muted?: boolean;
34
+ };
35
+ export type TiInfoSection = {
36
+ title: string;
37
+ /**
38
+ * Optional intro line under the section title.
39
+ */
40
+ description?: string;
41
+ /**
42
+ * Optional `ti-icon` variant name for the section head.
43
+ */
44
+ icon?: string;
45
+ /**
46
+ * Claim the full row of the two-up section grid instead of one column.
47
+ */
48
+ wide?: boolean;
49
+ /**
50
+ * A section with no items is dropped rather than rendered empty.
51
+ */
52
+ items: TiInfoItem[];
53
+ };
54
+ export type TiProfileIdentity = {
55
+ name: string;
56
+ /**
57
+ * Meta line under the name (e.g. `role family · specialization · unit`).
58
+ */
59
+ subtitle?: string;
60
+ /**
61
+ * Secondary line under the subtitle (e.g. the corporate e-mail).
62
+ */
63
+ caption?: string;
64
+ /**
65
+ * Stable seed for the deterministic avatar colour; defaults to the name.
66
+ */
67
+ avatarSeed?: string;
68
+ /**
69
+ * Small qualifier rendered inside the meta line.
70
+ */
71
+ badge?: {
72
+ text: string;
73
+ tone?: string;
74
+ };
75
+ /**
76
+ * Pills beside the name.
77
+ */
78
+ tags?: Array<{
79
+ text: string;
80
+ tone?: string;
81
+ dot?: boolean;
82
+ mono?: boolean;
83
+ }>;
84
+ };
85
+ export type TiProfileInfo = {
86
+ identity: TiProfileIdentity;
87
+ sections: TiInfoSection[];
88
+ };
89
+ export type TiApplicationInfo = {
90
+ /**
91
+ * Display name of the application.
92
+ */
93
+ name: string;
94
+ /**
95
+ * The npm package name it was resolved from.
96
+ */
97
+ packageName: string;
98
+ version: string;
99
+ releaseDate: string;
100
+ description: string;
101
+ license: string;
102
+ homepage: string;
103
+ author: string;
104
+ /**
105
+ * Framework component versions.
106
+ */
107
+ components: Array<{
108
+ name: string;
109
+ version: string;
110
+ }>;
111
+ /**
112
+ * Runtime facts (node/platform/instance), or `null` when withheld.
113
+ */
114
+ runtime: Object | null;
115
+ /**
116
+ * Application-contributed extra sections.
117
+ */
118
+ sections: TiInfoSection[];
119
+ };
14
120
  /** @import { TiLocalizationLanguage } from "@ti-engine/core/localization" */
15
121
  /**
16
122
  * @callback TiSessionCallback
@@ -29,3 +135,63 @@ export type TiSession = {
29
135
  * @property {(callback: TiSessionCallback) => TiSession} destroy
30
136
  * @property {(callback?: TiSessionCallback) => TiSession} save
31
137
  */
138
+ /**
139
+ * One label/value pair inside a {@link TiInfoSection}. Both strings are display-ready — already localized and
140
+ * already formatted by the server, since that is where the session language and the label catalogue live. The
141
+ * three flags are purely presentational; an empty `value` renders the screen's placeholder.
142
+ *
143
+ * @typedef {Object} TiInfoItem
144
+ * @property {string} label
145
+ * @property {string} [value]
146
+ * @property {string} [href] Renders the value as a link to this target. Only `http:`, `https:` and `mailto:` are
147
+ * honoured — any other scheme is dropped client-side and the item degrades to plain text.
148
+ * @property {boolean} [wide] Span the full width of the section grid instead of one column.
149
+ * @property {boolean} [mono] Render the value in the monospaced face (IDs, versions, hashes).
150
+ * @property {boolean} [muted] Render the value as a dimmed hint rather than primary text.
151
+ */
152
+ /**
153
+ * A titled group of label/value pairs. The framework's Profile and About screens render an array of these
154
+ * generically, so an application contributes content without contributing layout.
155
+ *
156
+ * @typedef {Object} TiInfoSection
157
+ * @property {string} title
158
+ * @property {string} [description] Optional intro line under the section title.
159
+ * @property {string} [icon] Optional `ti-icon` variant name for the section head.
160
+ * @property {boolean} [wide] Claim the full row of the two-up section grid instead of one column.
161
+ * @property {TiInfoItem[]} items A section with no items is dropped rather than rendered empty.
162
+ */
163
+ /**
164
+ * The identity header of the Profile screen — the avatar/name block and the pills beside it.
165
+ *
166
+ * @typedef {Object} TiProfileIdentity
167
+ * @property {string} name
168
+ * @property {string} [subtitle] Meta line under the name (e.g. `role family · specialization · unit`).
169
+ * @property {string} [caption] Secondary line under the subtitle (e.g. the corporate e-mail).
170
+ * @property {string} [avatarSeed] Stable seed for the deterministic avatar colour; defaults to the name.
171
+ * @property {{text: string, tone?: string}} [badge] Small qualifier rendered inside the meta line.
172
+ * @property {Array<{text: string, tone?: string, dot?: boolean, mono?: boolean}>} [tags] Pills beside the name.
173
+ */
174
+ /**
175
+ * The descriptor backing the framework Profile screen.
176
+ *
177
+ * @typedef {Object} TiProfileInfo
178
+ * @property {TiProfileIdentity} identity
179
+ * @property {TiInfoSection[]} sections
180
+ */
181
+ /**
182
+ * The descriptor backing the framework About screen. Produced by `buildApplicationInfo` and optionally extended by
183
+ * the application through {@link TiWebAppManager#getApplicationInfo}.
184
+ *
185
+ * @typedef {Object} TiApplicationInfo
186
+ * @property {string} name Display name of the application.
187
+ * @property {string} packageName The npm package name it was resolved from.
188
+ * @property {string} version
189
+ * @property {string} releaseDate
190
+ * @property {string} description
191
+ * @property {string} license
192
+ * @property {string} homepage
193
+ * @property {string} author
194
+ * @property {Array<{name: string, version: string}>} components Framework component versions.
195
+ * @property {Object|null} runtime Runtime facts (node/platform/instance), or `null` when withheld.
196
+ * @property {TiInfoSection[]} sections Application-contributed extra sections.
197
+ */
@@ -0,0 +1,111 @@
1
+ declare const _exports: {
2
+ ALGORITHM: string;
3
+ CACHE_KEY: string;
4
+ HASH_DEFAULTS: Readonly<{
5
+ N: 16384;
6
+ r: 8;
7
+ p: 1;
8
+ saltBytes: 16;
9
+ keyBytes: 64;
10
+ }>;
11
+ hashPassword: typeof hashPassword;
12
+ verifyPassword: typeof verifyPassword;
13
+ parseRecords: typeof parseRecords;
14
+ reconcile: typeof reconcile;
15
+ findByUsername: typeof findByUsername;
16
+ };
17
+ export = _exports;
18
+ export type LocalUserRecord = {
19
+ userID: string;
20
+ username: string;
21
+ email: string;
22
+ name: string;
23
+ passwordHash: string;
24
+ disabled: boolean;
25
+ };
26
+ /**
27
+ * Hashes a password for storage in a local-users file. Synchronous because its only caller is the one-shot CLI,
28
+ * where blocking is free — never call it on a request path.
29
+ *
30
+ * @method
31
+ * @param {string} password
32
+ * @returns {string} The encoded hash: `scrypt$N$r$p$salt$hash`, base64 salt and key.
33
+ * @throws {TypeError} If `password` is empty or not a string — `verifyPassword` refuses empty passwords, so
34
+ * hashing one here would only mint a hash that can never be logged into.
35
+ * @public
36
+ */
37
+ declare function hashPassword(password: string): string;
38
+ /**
39
+ * Verifies a password against an encoded hash. The cost parameters come from the stored string rather than the
40
+ * current defaults, so raising the defaults never invalidates an existing hash.
41
+ *
42
+ * @method
43
+ * @param {string} password
44
+ * @param {string} encoded
45
+ * @returns {Promise<boolean>} `false` for a malformed encoding or an absent password — never a throw, because a
46
+ * bad stored value must read as "does not match", not as a server error on the login path.
47
+ * @public
48
+ */
49
+ declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
50
+ /**
51
+ * Validates raw file content into records, reporting why any entry was excluded. Never throws: a malformed row is
52
+ * data, not a crash, so one bad entry cannot take an instance down.
53
+ *
54
+ * @method
55
+ * @param {*} raw
56
+ * @returns {{records: LocalUserRecord[], problems: string[]}}
57
+ * @public
58
+ */
59
+ declare function parseRecords(raw: any): {
60
+ records: LocalUserRecord[];
61
+ problems: string[];
62
+ };
63
+ /**
64
+ * Writes the records as the complete directory, keyed by username, and reports what changed.
65
+ * <br/>
66
+ * The whole set is written rather than patched because the file is the source of truth: a username absent from
67
+ * `records` must disappear, which is what makes revocation-by-file-edit work. `@ti-engine/core/cache` exposes no
68
+ * delete, so a whole-object write is also the only way to remove a key.
69
+ * <br/>
70
+ * Usernames are attacker-influenceable (the local sign-in handler resolves them from request input), so both the write
71
+ * and every read below are guarded against `Object.prototype`'s reserved names rather than trusting plain bracket
72
+ * access:
73
+ * <br/>
74
+ * - `incoming` is built with a null prototype (`Object.create( null )`) so it inherits nothing. On an ordinary
75
+ * `{}`, `incoming[ "__proto__" ] = record` would not create an own key at all — it would invoke the inherited
76
+ * `__proto__` setter and silently repoint the object's own prototype to `record`, so the record never shows up
77
+ * in `Object.keys`/`JSON.stringify` and is never persisted, without error. On a null-prototype object that
78
+ * setter does not exist anywhere on the (empty) prototype chain, so the assignment falls back to creating a
79
+ * perfectly ordinary own data property instead — confirmed empirically (see the test file) that this still
80
+ * `JSON.stringify`s and round-trips normally.
81
+ * - Every classification read below checks ownership with `Object.prototype.hasOwnProperty.call(...)` rather than
82
+ * relying on truthiness, because `stored` comes back from `readStored()` — ultimately a `JSON.parse` result —
83
+ * with the ordinary `Object.prototype` chain. An unguarded `stored[ "constructor" ]` would resolve to the
84
+ * inherited `Object` constructor function (always truthy) rather than "not present", misclassifying a
85
+ * first-time `constructor`-named user as `updated` instead of `added`, and hiding its removal from `removed`.
86
+ *
87
+ * @method
88
+ * @param {LocalUserRecord[]} records
89
+ * @returns {Promise<{added: string[], updated: string[], removed: string[]}>}
90
+ * @public
91
+ */
92
+ declare function reconcile(records: LocalUserRecord[]): Promise<{
93
+ added: string[];
94
+ updated: string[];
95
+ removed: string[];
96
+ }>;
97
+ /**
98
+ * Looks a user up by exact username.
99
+ * <br/>
100
+ * `username` here is attacker-influenceable — this is the function the local sign-in handler calls with the
101
+ * value a client typed into the username field. Checked with `Object.prototype.hasOwnProperty.call(...)` rather than
102
+ * `stored[ username ] || null`, because `stored` carries the ordinary `Object.prototype` chain and an unguarded
103
+ * bracket read would resolve `findByUsername( "constructor" )` to the inherited `Object` constructor function
104
+ * instead of `null`, violating the declared return type for nearly every real query.
105
+ *
106
+ * @method
107
+ * @param {string} username
108
+ * @returns {Promise<LocalUserRecord|null>}
109
+ * @public
110
+ */
111
+ declare function findByUsername(username: string): Promise<LocalUserRecord | null>;
@@ -4,7 +4,7 @@ export = applyWebConfigEnvOverrides;
4
4
  * Each override is applied ONLY when its environment variable is defined, so an absent variable leaves the
5
5
  * configured/default value untouched (fully backward compatible). This gives ti-engine web servers 12-factor,
6
6
  * container-friendly control over network binding, TLS, the session cookie secret, the enabled authentication
7
- * methods, the admin allowlist, the trusted request origins, and the `/static` cache policy without editing config files. Note `TI_WEB_AUTH_METHODS`,
7
+ * methods, the admin allowlist, the local auth users file path, the trusted request origins, and the `/static` cache policy without editing config files. Note `TI_WEB_AUTH_METHODS`,
8
8
  * `TI_WEB_AUTH_ADMINS`, `TI_WEB_TRUSTED_ORIGINS`, and `TI_WEB_STATIC_IMMUTABLE_PATHS` fully REPLACE their config arrays (`auth.enabledMethods` / `auth.admins` / `trustedOrigins` / `staticCache.immutablePaths`) rather than
9
9
  * merging — the config-file merge is by-index and cannot cleanly override an array.
10
10
  *