@ti-engine/web-framework 1.21.0 → 1.24.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.
@@ -191,6 +191,65 @@ declare class ConfigService {
191
191
  value: Object;
192
192
  }>;
193
193
  }>;
194
+ /**
195
+ * Compares a document's registered file default against the value currently in the store. This is how a
196
+ * configuration change shipped in a release becomes visible on a deployment that was seeded before it — the
197
+ * store seeds only once, so a later file change is otherwise invisible.
198
+ *
199
+ * @method
200
+ * @param {string} configKey
201
+ * @returns {Promise<{configKey: string, status: string, counts: Object, entries: Array, storedVersion: number, editable: boolean, label: string}>}
202
+ * @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
203
+ * @public
204
+ */
205
+ getDrift(configKey: string): Promise<{
206
+ configKey: string;
207
+ status: string;
208
+ counts: Object;
209
+ entries: any[];
210
+ storedVersion: number;
211
+ editable: boolean;
212
+ label: string;
213
+ }>;
214
+ /**
215
+ * Drift summaries for every registered document. This still computes each document's full entry list internally
216
+ * (it delegates to {@link ConfigService#getDrift} per document) — the saving is in the response shape, not the
217
+ * computation: `entries` is omitted here to keep the payload small enough for a landing screen and a startup log,
218
+ * where only the counts are shown.
219
+ *
220
+ * @method
221
+ * @returns {Promise<Array<Object>>}
222
+ * @public
223
+ */
224
+ listDrift(): Promise<Array<Object>>;
225
+ /**
226
+ * Applies the registered file defaults for the given documents, as a single validated change-set.
227
+ * <br/>
228
+ * Routing through {@link ConfigService#applyEdits} is deliberate: the application is schema- and
229
+ * semantically validated, versioned, correlated into one change-set, added to the audit feed, and restorable —
230
+ * and, because a validator sees its siblings at their *pending* value, interdependent documents applied
231
+ * together validate against each other rather than against the stale stored state.
232
+ *
233
+ * @method
234
+ * @param {string[]} configKeys
235
+ * @param {Object} meta
236
+ * @param {string} meta.adminID
237
+ * @param {string} [meta.note]
238
+ * @returns {Promise<{ok: true, changeSetID: string, versions: Object}|{ok: false, errors: Object}>}
239
+ * @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On bad input, an unknown key, or a key with no default.
240
+ * @public
241
+ */
242
+ applyDefaults(configKeys: string[], meta: {
243
+ adminID: string;
244
+ note?: string;
245
+ }): Promise<{
246
+ ok: true;
247
+ changeSetID: string;
248
+ versions: Object;
249
+ } | {
250
+ ok: false;
251
+ errors: Object;
252
+ }>;
194
253
  /**
195
254
  * Seeds a document's default value into the store only if it has never been written (idempotent bootstrap).
196
255
  * Used by an application to bring its file defaults into the store at startup before serving live config.
@@ -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
  *