@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.
- package/CHANGELOG.md +82 -0
- package/README.md +52 -0
- package/bin/build/hash-password.js +36 -0
- package/bin/config/local-users.example.json +8 -0
- package/bin/localization/web-server-labels.json +4 -0
- package/bin/static/fragments/frame-login.html +6 -1
- package/bin/static/scripts/ti-framework.css +4 -0
- package/bin/static/scripts/ti-framework.js +23 -0
- package/bin/web-server.js +21 -1
- package/bin/web-server.json +3 -0
- package/components/admin-config-handlers.js +31 -0
- package/components/auth-manager.js +159 -18
- package/components/config-drift.js +141 -0
- package/components/config-service.js +97 -0
- package/components/local-user-directory.js +428 -0
- package/components/web-config-env.js +6 -1
- package/components/web-handlers.js +10 -2
- package/package.json +19 -2
- package/types/bin/web-server.d.ts +19 -1
- package/types/components/admin-config-handlers.d.ts +3 -0
- package/types/components/auth-manager.d.ts +7 -0
- package/types/components/config-drift.d.ts +27 -0
- package/types/components/config-service.d.ts +59 -0
- package/types/components/local-user-directory.d.ts +111 -0
- package/types/components/web-config-env.d.ts +1 -1
|
@@ -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
|
*
|