@ti-engine/web-framework 1.19.1 → 1.20.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 +29 -0
- package/bin/web-app-manager.js +3 -6
- package/bin/web-server.js +8 -9
- package/components/admin-config-handlers.js +3 -0
- package/components/auth-manager.js +3 -7
- package/components/authorization.js +2 -2
- package/components/config-change-notifier.js +4 -4
- package/components/config-registry.js +3 -6
- package/components/config-service.js +9 -6
- package/components/config-store.js +1 -3
- package/components/definitions.types.js +5 -3
- package/components/session-store.js +9 -6
- package/components/user.js +2 -0
- package/components/web-handlers.js +7 -4
- package/package.json +84 -21
- package/types/bin/web-app-manager.d.ts +194 -0
- package/types/bin/web-server.d.ts +374 -0
- package/types/components/admin-config-handlers.d.ts +11 -0
- package/types/components/auth-manager.d.ts +125 -0
- package/types/components/authorization.d.ts +54 -0
- package/types/components/config-change-notifier.d.ts +73 -0
- package/types/components/config-registry.d.ts +149 -0
- package/types/components/config-service.d.ts +218 -0
- package/types/components/config-store.d.ts +128 -0
- package/types/components/definitions.types.d.ts +31 -0
- package/types/components/session-store.d.ts +56 -0
- package/types/components/user.d.ts +83 -0
- package/types/components/web-config-env.d.ts +17 -0
- package/types/components/web-handlers.d.ts +23 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export = AuthManager;
|
|
2
|
+
import User = require("#user");
|
|
3
|
+
import type { SettingsAuth } from "#web-server";
|
|
4
|
+
export type TiAuthMethod = string;
|
|
5
|
+
/** @import { SettingsAuth } from "#web-server" */
|
|
6
|
+
/**
|
|
7
|
+
* Enum for specifying the authentication method.
|
|
8
|
+
*
|
|
9
|
+
* @readonly
|
|
10
|
+
* @enum {string}
|
|
11
|
+
* @typedef {string} TiAuthMethod
|
|
12
|
+
*/
|
|
13
|
+
declare const authMethodEnum: import("@ti-engine/core/definitions").TiEnumOf<{
|
|
14
|
+
LOCAL: string[];
|
|
15
|
+
OPENID_AZURE: string[];
|
|
16
|
+
OPENID_GOOGLE: string[];
|
|
17
|
+
}>;
|
|
18
|
+
export type TiTokenEndpointAuthMethod = string;
|
|
19
|
+
/**
|
|
20
|
+
* The AuthManager class is used to manage authentication and authorization.
|
|
21
|
+
*
|
|
22
|
+
* @class AuthManager
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
declare class AuthManager {
|
|
26
|
+
#private;
|
|
27
|
+
/**
|
|
28
|
+
* @constructor
|
|
29
|
+
* @param {SettingsAuth} settings
|
|
30
|
+
*/
|
|
31
|
+
constructor(settings: SettingsAuth);
|
|
32
|
+
/**
|
|
33
|
+
* Used to initialize the authentication manager.
|
|
34
|
+
*
|
|
35
|
+
* @method
|
|
36
|
+
* @returns {Promise}
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
initialize(): Promise<any>;
|
|
40
|
+
/**
|
|
41
|
+
* Used to check whether the specified authentication method is enabled.
|
|
42
|
+
*
|
|
43
|
+
* @method
|
|
44
|
+
* @param {TiAuthMethod} authMethod
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
isAuthEnabled(authMethod: TiAuthMethod): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Returns the list of currently enabled authentication methods, reflecting any OpenID providers dropped by
|
|
51
|
+
* {@link AuthManager#initialize} for being enabled but unconfigured. Callers (e.g. the login-page renderer)
|
|
52
|
+
* use this to present only the methods a user can actually complete.
|
|
53
|
+
*
|
|
54
|
+
* @method
|
|
55
|
+
* @returns {TiAuthMethod[]}
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
getEnabledMethods(): TiAuthMethod[];
|
|
59
|
+
/**
|
|
60
|
+
* Used to authenticate a user via the specified authentication method.
|
|
61
|
+
*
|
|
62
|
+
* @method
|
|
63
|
+
* @param {TiAuthMethod} authMethod
|
|
64
|
+
* @param {Object} authDetails
|
|
65
|
+
* @returns {Promise<Object>}
|
|
66
|
+
* @throws {TiException.E_SEC_UNRECOGNIZED_AUTH_METHOD} If the authentication method is not recognized or enabled.
|
|
67
|
+
* @throws {TiException.E_GEN_NOT_INITIALIZED} If the auth manager was not properly initialized.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
authenticate(authMethod: TiAuthMethod, authDetails: Object): Promise<Object>;
|
|
71
|
+
/**
|
|
72
|
+
* Used to set up user authorization according to the specified authentication method.
|
|
73
|
+
*
|
|
74
|
+
* @method
|
|
75
|
+
* @param {TiAuthMethod} authMethod
|
|
76
|
+
* @param {URL} currentUrl
|
|
77
|
+
* @param {Object} oidc
|
|
78
|
+
* @returns {Promise<User>}
|
|
79
|
+
* @throws {TiException.E_SEC_UNRECOGNIZED_AUTH_METHOD} If the authentication method is not recognized.
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
authorize(authMethod: TiAuthMethod, currentUrl: URL, oidc: Object): Promise<User>;
|
|
83
|
+
/**
|
|
84
|
+
* Used to get the callback URL for the specified OAuth2 authentication method.
|
|
85
|
+
*
|
|
86
|
+
* @method
|
|
87
|
+
* @param {TiAuthMethod} authMethod
|
|
88
|
+
* @returns {string}
|
|
89
|
+
* @throws {TiException.E_SEC_UNRECOGNIZED_AUTH_METHOD} If the requested OAuth2 method is not recognized or enabled.
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
getOAuth2CallbackUrl(authMethod: TiAuthMethod): string;
|
|
93
|
+
/**
|
|
94
|
+
* Used to get the local route path of the callback for the specified OAuth2 authentication method.
|
|
95
|
+
* <br/>
|
|
96
|
+
* A callback can legitimately be configured either as a path or as the full absolute URL registered with the
|
|
97
|
+
* identity provider. The absolute form is what the provider expects as the redirect URI, but it is not a usable
|
|
98
|
+
* Express route pattern, so this reduces whatever is configured to the path the server must actually listen on.
|
|
99
|
+
*
|
|
100
|
+
* @method
|
|
101
|
+
* @param {TiAuthMethod} authMethod
|
|
102
|
+
* @returns {string|null} The route path, or null if the configured callback yields no usable path.
|
|
103
|
+
* @throws {TiException.E_SEC_UNRECOGNIZED_AUTH_METHOD} If the requested OAuth2 method is not recognized or enabled.
|
|
104
|
+
* @public
|
|
105
|
+
*/
|
|
106
|
+
getOAuth2CallbackPath(authMethod: TiAuthMethod): string | null;
|
|
107
|
+
/**
|
|
108
|
+
* Reduces a configured OAuth2 callback value to the local route path it corresponds to. Accepts an absolute URL
|
|
109
|
+
* ('https://host/login/azure-callback'), a protocol-relative URL, or a path with or without its leading slash,
|
|
110
|
+
* and strips any query string or fragment. Pure and static; exposed for unit testing.
|
|
111
|
+
* <br/>
|
|
112
|
+
* NOTE: This exists because Express 5 parses a route pattern with path-to-regexp v8, where ':' opens a parameter
|
|
113
|
+
* name — so an absolute URL used verbatim as a route path throws 'Missing parameter name' at startup.
|
|
114
|
+
*
|
|
115
|
+
* @method
|
|
116
|
+
* @static
|
|
117
|
+
* @param {string} callbackUrl
|
|
118
|
+
* @returns {string|null} The route path, or null if no usable path can be derived.
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
static toCallbackPath(callbackUrl: string): string | null;
|
|
122
|
+
}
|
|
123
|
+
declare namespace AuthManager {
|
|
124
|
+
export { authMethodEnum as authMethod };
|
|
125
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
declare const _exports: {
|
|
2
|
+
ADMIN_ROLE: string;
|
|
3
|
+
isAdminIdentity: typeof isAdminIdentity;
|
|
4
|
+
applyAdminRole: typeof applyAdminRole;
|
|
5
|
+
hasAnyRole: typeof hasAnyRole;
|
|
6
|
+
isAccessAllowed: typeof isAccessAllowed;
|
|
7
|
+
requireRole: typeof requireRole;
|
|
8
|
+
requireAdmin: (request: Object, response: Object, next: Function) => void;
|
|
9
|
+
};
|
|
10
|
+
export = _exports;
|
|
11
|
+
/**
|
|
12
|
+
* Returns `true` if the user matches any entry in the admin allowlist. An entry may match the user's `userID`,
|
|
13
|
+
* `username`, or `email` (case-insensitive).
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} user A session user (`{ userID, username, email, roles, ... }`).
|
|
16
|
+
* @param {string[]} admins The configured allowlist of admin identifiers.
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
declare function isAdminIdentity(user: Object, admins: string[]): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Adds the `admin` role to the session user (additively, no duplicates) when the user is in the allowlist.
|
|
22
|
+
* Safe to call with an empty/missing allowlist or session — it is then a no-op. Returns the session for chaining.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} session
|
|
25
|
+
* @param {string[]} [admins]
|
|
26
|
+
* @returns {Object} The (possibly modified) session.
|
|
27
|
+
*/
|
|
28
|
+
declare function applyAdminRole(session: Object, admins?: string[]): Object;
|
|
29
|
+
/**
|
|
30
|
+
* @param {Object} session
|
|
31
|
+
* @param {Array<string|number>} roles
|
|
32
|
+
* @returns {boolean} `true` if the session user holds any of the given roles.
|
|
33
|
+
*/
|
|
34
|
+
declare function hasAnyRole(session: Object, roles: Array<string | number>): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Pure access decision for a resource (e.g. an HTML fragment) that declares a set of required roles. A resource with
|
|
37
|
+
* no required roles (`null` / `undefined` / empty) is public — any (authenticated) user may access it; otherwise the
|
|
38
|
+
* user must hold at least one of the required roles. Roles are treated opaquely, so this works equally for numeric
|
|
39
|
+
* application role codes and the string `admin` role — there is no implicit hierarchy (an `admin`-gated resource is
|
|
40
|
+
* reachable only by holders of the `admin` role, never by a high numeric role). Backs {@link TiWebAppManager#verifyAccess}.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<string|number>} [requiredRoles] The roles permitted to access the resource; empty/absent = public.
|
|
43
|
+
* @param {Array<string|number>} [userRoles] The roles held by the current session user.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
declare function isAccessAllowed(requiredRoles?: Array<string | number>, userRoles?: Array<string | number>): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Express middleware factory that admits a request only if its session user holds at least one of the given roles.
|
|
49
|
+
* Responds `401` when unauthenticated (no session user) and `403` when authenticated but lacking the role.
|
|
50
|
+
*
|
|
51
|
+
* @param {...(string|number)} roles
|
|
52
|
+
* @returns {(request: Object, response: Object, next: Function) => void}
|
|
53
|
+
*/
|
|
54
|
+
declare function requireRole(...roles: (string | number)[]): (request: Object, response: Object, next: Function) => void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export = ConfigChangeNotifier;
|
|
2
|
+
export type ConfigChangeEvent = {
|
|
3
|
+
changeSetID: string;
|
|
4
|
+
/**
|
|
5
|
+
* The configuration documents affected by the change.
|
|
6
|
+
*/
|
|
7
|
+
configKeys: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Who committed the change.
|
|
10
|
+
*/
|
|
11
|
+
adminID: string;
|
|
12
|
+
/**
|
|
13
|
+
* ISO timestamp.
|
|
14
|
+
*/
|
|
15
|
+
timestamp: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} ConfigChangeEvent
|
|
19
|
+
* @property {string} changeSetID
|
|
20
|
+
* @property {string[]} configKeys The configuration documents affected by the change.
|
|
21
|
+
* @property {string} adminID Who committed the change.
|
|
22
|
+
* @property {string} timestamp ISO timestamp.
|
|
23
|
+
*/
|
|
24
|
+
declare const CONFIG_CHANGED = "config:changed";
|
|
25
|
+
/**
|
|
26
|
+
* Notifies subscribers that configuration changed, so they can react (e.g. invalidate an in-memory cache, or push a
|
|
27
|
+
* live update to an admin UI). This is the **in-process** implementation of a deliberately transport-agnostic
|
|
28
|
+
* contract — `publish(event)` (fire-and-forget) and `subscribe(listener) → unsubscribe`.
|
|
29
|
+
*
|
|
30
|
+
* **Designed for an eventual switch to a reusable core pub/sub.** Cross-instance propagation is out of scope for v1
|
|
31
|
+
* (the store-backed model already makes a committed change visible to every instance via the shared Redis cache;
|
|
32
|
+
* this emitter exists to invalidate optional *in-memory* caches and drive live UI within a process). When a Redis
|
|
33
|
+
* (or other) pub/sub primitive lands in `@ti-engine/core`, a drop-in implementation of this same contract can be
|
|
34
|
+
* provided and injected into {@link ConfigService} — no change to publishers or subscribers. To keep that swap
|
|
35
|
+
* behavior-safe, **delivery here is already asynchronous** (matching cross-instance transports); subscribers must
|
|
36
|
+
* not assume synchronous delivery, and the event payload is plain serializable JSON so it survives a wire transport.
|
|
37
|
+
*
|
|
38
|
+
* @class ConfigChangeNotifier
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
declare class ConfigChangeNotifier {
|
|
42
|
+
#private;
|
|
43
|
+
constructor();
|
|
44
|
+
/**
|
|
45
|
+
* Publishes a configuration-change event to all subscribers. Fire-and-forget; delivery is asynchronous.
|
|
46
|
+
*
|
|
47
|
+
* @method
|
|
48
|
+
* @param {ConfigChangeEvent} event
|
|
49
|
+
* @returns {ConfigChangeEvent} The (frozen) event that will be delivered.
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
52
|
+
publish(event: ConfigChangeEvent): ConfigChangeEvent;
|
|
53
|
+
/**
|
|
54
|
+
* Subscribes a listener to configuration-change events.
|
|
55
|
+
*
|
|
56
|
+
* @method
|
|
57
|
+
* @param {(event: ConfigChangeEvent) => void} listener
|
|
58
|
+
* @returns {() => void} An unsubscribe function.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
subscribe(listener: (event: ConfigChangeEvent) => void): () => void;
|
|
62
|
+
/**
|
|
63
|
+
* @method
|
|
64
|
+
* @returns {number} The current number of subscribers.
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
subscriberCount(): number;
|
|
68
|
+
}
|
|
69
|
+
declare namespace ConfigChangeNotifier {
|
|
70
|
+
export { instance };
|
|
71
|
+
export { CONFIG_CHANGED };
|
|
72
|
+
}
|
|
73
|
+
declare const instance: ConfigChangeNotifier;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export = ConfigRegistry;
|
|
2
|
+
export type ConfigValidationIssue = {
|
|
3
|
+
/**
|
|
4
|
+
* A JSON pointer / data path to the offending value (e.g. ".competencies.E1-1.name"), or "".
|
|
5
|
+
*/
|
|
6
|
+
path: string;
|
|
7
|
+
/**
|
|
8
|
+
* Human-readable problem description.
|
|
9
|
+
*/
|
|
10
|
+
message: string;
|
|
11
|
+
/**
|
|
12
|
+
* "schema" for JSON-Schema failures, or a validator-supplied code (default "semantic").
|
|
13
|
+
*/
|
|
14
|
+
code: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional structured details (e.g. ajv params).
|
|
17
|
+
*/
|
|
18
|
+
params?: Object;
|
|
19
|
+
};
|
|
20
|
+
export type ValidatorContext = {
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the *pending* value of `key` when it is part of the
|
|
23
|
+
* current edit batch, otherwise its current committed value. Lets a validator check a sibling document's
|
|
24
|
+
* post-edit state — but calling this for the document being validated itself just returns the same incoming
|
|
25
|
+
* value already passed as the validator's first argument, not its prior state.
|
|
26
|
+
*/
|
|
27
|
+
getConfig: (key: string) => Promise<any>;
|
|
28
|
+
/**
|
|
29
|
+
* Always resolves the current committed value of `key`,
|
|
30
|
+
* even when `key` is the document currently under validation. Use this to compare a document against its own
|
|
31
|
+
* previous state (e.g. detecting an edit that should have bumped a version marker).
|
|
32
|
+
*/
|
|
33
|
+
getStoredConfig: (key: string) => Promise<any>;
|
|
34
|
+
};
|
|
35
|
+
export type SemanticValidator = (value: Object, context: ValidatorContext) => ConfigValidationIssue[] | Promise<ConfigValidationIssue[]>;
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ConfigValidationIssue
|
|
38
|
+
* @property {string} path A JSON pointer / data path to the offending value (e.g. ".competencies.E1-1.name"), or "".
|
|
39
|
+
* @property {string} message Human-readable problem description.
|
|
40
|
+
* @property {string} code "schema" for JSON-Schema failures, or a validator-supplied code (default "semantic").
|
|
41
|
+
* @property {Object} [params] Optional structured details (e.g. ajv params).
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
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 {(key: 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 {(key: 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
|
+
* @typedef {(value: Object, context: ValidatorContext) => ConfigValidationIssue[]|Promise<ConfigValidationIssue[]>} SemanticValidator
|
|
58
|
+
* A semantic validator receives the candidate value and a {@link ValidatorContext} and returns the issues it found
|
|
59
|
+
* (empty array = OK). May be async.
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Registry of editable configuration *documents* and the gate that validates a candidate value against a
|
|
63
|
+
* document's JSON Schema (ajv) plus its semantic validators. The framework stays domain-agnostic: an application
|
|
64
|
+
* registers its config documents (schemas, validators, defaults, editor metadata) at startup; this component knows
|
|
65
|
+
* only "validated, versioned JSON documents". Validation must pass *before* {@link ConfigStore#saveChangeSet}.
|
|
66
|
+
*
|
|
67
|
+
* @class ConfigRegistry
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
declare class ConfigRegistry {
|
|
71
|
+
#private;
|
|
72
|
+
constructor();
|
|
73
|
+
/**
|
|
74
|
+
* Registers an editable configuration document.
|
|
75
|
+
*
|
|
76
|
+
* @method
|
|
77
|
+
* @param {string} configKey Stable identifier for the document (also the ConfigStore key).
|
|
78
|
+
* @param {Object} definition
|
|
79
|
+
* @param {Object} definition.schema A JSON Schema for the document.
|
|
80
|
+
* @param {SemanticValidator[]} [definition.validators] Cross-cutting/semantic checks beyond the schema.
|
|
81
|
+
* @param {Object} [definition.defaultValue] The bootstrap default (seeds an empty store).
|
|
82
|
+
* @param {Object} [definition.metadata] Editor metadata (label, editor type, group, risk class, …).
|
|
83
|
+
* @returns {ConfigRegistry} this (chainable)
|
|
84
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On missing key/schema.
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
register(configKey: string, definition: {
|
|
88
|
+
schema: Object;
|
|
89
|
+
validators?: SemanticValidator[];
|
|
90
|
+
defaultValue?: Object;
|
|
91
|
+
metadata?: Object;
|
|
92
|
+
}): ConfigRegistry;
|
|
93
|
+
/**
|
|
94
|
+
* Adds a schema that is referenced (via `$ref`/`$id`) by document schemas but is not itself a document.
|
|
95
|
+
*
|
|
96
|
+
* @method
|
|
97
|
+
* @param {Object} schema
|
|
98
|
+
* @returns {ConfigRegistry} this (chainable)
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
addSchema(schema: Object): ConfigRegistry;
|
|
102
|
+
/**
|
|
103
|
+
* @method
|
|
104
|
+
* @param {string} configKey
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
has(configKey: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* @method
|
|
111
|
+
* @returns {string[]} All registered configuration keys.
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
list(): string[];
|
|
115
|
+
/**
|
|
116
|
+
* @method
|
|
117
|
+
* @param {string} configKey
|
|
118
|
+
* @returns {Object|undefined} The editor metadata registered for the document.
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
metadataFor(configKey: string): Object | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* @method
|
|
124
|
+
* @param {string} configKey
|
|
125
|
+
* @returns {Object|undefined} The bootstrap default value registered for the document.
|
|
126
|
+
* @public
|
|
127
|
+
*/
|
|
128
|
+
getDefault(configKey: string): Object | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Validates a candidate value for a registered document: JSON Schema first, then the semantic validators.
|
|
131
|
+
* Resolves with `{ valid, errors }` (errors is an array of {@link ConfigValidationIssue}).
|
|
132
|
+
*
|
|
133
|
+
* @method
|
|
134
|
+
* @param {string} configKey
|
|
135
|
+
* @param {Object} value
|
|
136
|
+
* @param {ValidatorContext} [context] Passed to each semantic validator.
|
|
137
|
+
* @returns {Promise<{valid: boolean, errors: ConfigValidationIssue[]}>}
|
|
138
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
|
|
139
|
+
* @public
|
|
140
|
+
*/
|
|
141
|
+
validate(configKey: string, value: Object, context?: ValidatorContext): Promise<{
|
|
142
|
+
valid: boolean;
|
|
143
|
+
errors: ConfigValidationIssue[];
|
|
144
|
+
}>;
|
|
145
|
+
}
|
|
146
|
+
declare namespace ConfigRegistry {
|
|
147
|
+
export { instance };
|
|
148
|
+
}
|
|
149
|
+
declare const instance: ConfigRegistry;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export = ConfigService;
|
|
2
|
+
import type ConfigChangeNotifier from "#config-change-notifier";
|
|
3
|
+
import type ConfigRegistry from "#config-registry";
|
|
4
|
+
import type ConfigStore from "#config-store";
|
|
5
|
+
/** @import ConfigChangeNotifier from "#config-change-notifier" */
|
|
6
|
+
/** @import ConfigRegistry from "#config-registry" */
|
|
7
|
+
/** @import ConfigStore from "#config-store" */
|
|
8
|
+
/**
|
|
9
|
+
* Orchestrates validated, versioned configuration edits on top of {@link ConfigStore} and {@link ConfigRegistry}.
|
|
10
|
+
*
|
|
11
|
+
* Two layers:
|
|
12
|
+
* - **Document level** — {@link ConfigService#applyEdits}: validate every affected document (schema + semantic,
|
|
13
|
+
* with a cross-document {@link ValidatorContext} whose `getConfig` sees the *pending* values of the same edit —
|
|
14
|
+
* letting a validator check a sibling document's post-edit state — while `getStoredConfig` always returns the
|
|
15
|
+
* committed value, even for the document currently under validation) and, only if all pass, commit them as one
|
|
16
|
+
* change-set. Validation failures return `{ ok:false, errors }` and write nothing; a version conflict from the
|
|
17
|
+
* store surfaces as a rejection.
|
|
18
|
+
* - **Entity level** — composite editors registered with `compose(docs)→view` / `decompose(edited, docs)→{key:value}`,
|
|
19
|
+
* so the UI edits a domain entity (e.g. a "competency") that is projected from, and scattered back into, several
|
|
20
|
+
* documents. {@link ConfigService#saveEditorEdit} decomposes the edit and routes it through `applyEdits`.
|
|
21
|
+
*
|
|
22
|
+
* @class ConfigService
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
declare class ConfigService {
|
|
26
|
+
#private;
|
|
27
|
+
/**
|
|
28
|
+
* @constructor
|
|
29
|
+
* @param {Object} [options]
|
|
30
|
+
* @param {ConfigStore} [options.store] Defaults to the ConfigStore singleton.
|
|
31
|
+
* @param {ConfigRegistry} [options.registry] Defaults to the ConfigRegistry singleton.
|
|
32
|
+
* @param {ConfigChangeNotifier} [options.notifier] Defaults to the ConfigChangeNotifier singleton.
|
|
33
|
+
*/
|
|
34
|
+
constructor(options?: {
|
|
35
|
+
store?: ConfigStore;
|
|
36
|
+
registry?: ConfigRegistry;
|
|
37
|
+
notifier?: ConfigChangeNotifier;
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Validates and commits a set of document edits atomically. Each edit: `{ configKey, value, expectedVersion }`.
|
|
41
|
+
*
|
|
42
|
+
* @method
|
|
43
|
+
* @param {Array<{configKey: string, value: Object, expectedVersion: number}>} edits
|
|
44
|
+
* @param {Object} meta
|
|
45
|
+
* @param {string} meta.adminID
|
|
46
|
+
* @param {string} [meta.note]
|
|
47
|
+
* @returns {Promise<{ok: true, changeSetID: string, versions: Object<string, number>} | {ok: false, errors: Object<string, Array>}>}
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
applyEdits(edits: Array<{
|
|
51
|
+
configKey: string;
|
|
52
|
+
value: Object;
|
|
53
|
+
expectedVersion: number;
|
|
54
|
+
}>, meta: {
|
|
55
|
+
adminID: string;
|
|
56
|
+
note?: string;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
ok: true;
|
|
59
|
+
changeSetID: string;
|
|
60
|
+
versions: Record<string, number>;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
errors: Record<string, any[]>;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Registers a composite editor over one or more documents.
|
|
67
|
+
*
|
|
68
|
+
* @method
|
|
69
|
+
* @param {string} editorKey
|
|
70
|
+
* @param {Object} definition
|
|
71
|
+
* @param {string[]} definition.documents The configKeys this editor spans.
|
|
72
|
+
* @param {(configs: Object) => *} definition.compose Maps `{ [configKey]: value }` → a view for the UI.
|
|
73
|
+
* @param {(editedView: *, currentDocs: Object) => Object.<string, Object>} definition.decompose Maps `(editedView, currentDocs)` → the
|
|
74
|
+
* full new values for the documents that changed (`{ [configKey]: newValue }`).
|
|
75
|
+
* @param {Object} [definition.metadata]
|
|
76
|
+
* @returns {ConfigService} this (chainable)
|
|
77
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS}
|
|
78
|
+
* @public
|
|
79
|
+
*/
|
|
80
|
+
registerEditor(editorKey: string, definition: {
|
|
81
|
+
documents: string[];
|
|
82
|
+
compose: (configs: Object) => any;
|
|
83
|
+
decompose: (editedView: any, currentDocs: Object) => Record<string, Object>;
|
|
84
|
+
metadata?: Object;
|
|
85
|
+
}): ConfigService;
|
|
86
|
+
/**
|
|
87
|
+
* @method
|
|
88
|
+
* @param {string} editorKey
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
hasEditor(editorKey: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* @method
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
listEditors(): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Loads the editor's documents and composes them into a view. Returns the view plus the current per-document
|
|
101
|
+
* versions, which the client must echo back on save for optimistic locking.
|
|
102
|
+
*
|
|
103
|
+
* @method
|
|
104
|
+
* @param {string} editorKey
|
|
105
|
+
* @returns {Promise<{rows: *, versions: Object<string, number>}>}
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
composeView(editorKey: string): Promise<{
|
|
109
|
+
rows: any;
|
|
110
|
+
versions: Record<string, number>;
|
|
111
|
+
}>;
|
|
112
|
+
/**
|
|
113
|
+
* Applies an edit made against a composite editor: decompose the edited view into per-document new values, then
|
|
114
|
+
* route through {@link ConfigService#applyEdits} (validate-all → atomic change-set). `expectedVersions` should be
|
|
115
|
+
* the versions returned by {@link ConfigService#composeView} when the edit started.
|
|
116
|
+
*
|
|
117
|
+
* @method
|
|
118
|
+
* @param {string} editorKey
|
|
119
|
+
* @param {*} editedView
|
|
120
|
+
* @param {Object} meta
|
|
121
|
+
* @param {Object<string, number>} [expectedVersions]
|
|
122
|
+
* @returns {Promise<Object>} The {@link ConfigService#applyEdits} result (or `{ ok:true, changeSetID:null }` if nothing changed).
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
saveEditorEdit(editorKey: string, editedView: any, meta: Object, expectedVersions?: Record<string, number>): Promise<Object>;
|
|
126
|
+
/**
|
|
127
|
+
* Restores a prior change-set through the validated path: rebuild edits from the change-set's historic snapshots
|
|
128
|
+
* and route them through {@link ConfigService#applyEdits} — so the restore is **re-validated against the current
|
|
129
|
+
* schemas/validators** (a snapshot valid when written may be invalid now) and emits `config:changed`. Returns the
|
|
130
|
+
* `applyEdits` result (`{ ok:false, errors }` if a snapshot no longer validates; nothing is written then).
|
|
131
|
+
*
|
|
132
|
+
* @method
|
|
133
|
+
* @param {string} changeSetID
|
|
134
|
+
* @param {Object} meta
|
|
135
|
+
* @param {string} meta.adminID
|
|
136
|
+
* @param {string} [meta.note]
|
|
137
|
+
* @returns {Promise<Object>}
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
140
|
+
restoreChangeSet(changeSetID: string, meta: {
|
|
141
|
+
adminID: string;
|
|
142
|
+
note?: string;
|
|
143
|
+
}): Promise<Object>;
|
|
144
|
+
/**
|
|
145
|
+
* @method
|
|
146
|
+
* @param {string} configKey
|
|
147
|
+
* @returns {Promise<Object|null>} The current envelope for a configuration document.
|
|
148
|
+
* @public
|
|
149
|
+
*/
|
|
150
|
+
getCurrent(configKey: string): Promise<Object | null>;
|
|
151
|
+
/**
|
|
152
|
+
* @method
|
|
153
|
+
* @param {string} configKey
|
|
154
|
+
* @returns {Promise<Array<Object>>} The document's version history (ascending), each a full snapshot entry.
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
getHistory(configKey: string): Promise<Array<Object>>;
|
|
158
|
+
/**
|
|
159
|
+
* @method
|
|
160
|
+
* @param {string} changeSetID
|
|
161
|
+
* @returns {Promise<Object|null>} A single change-set record.
|
|
162
|
+
* @public
|
|
163
|
+
*/
|
|
164
|
+
getChange(changeSetID: string): Promise<Object | null>;
|
|
165
|
+
/**
|
|
166
|
+
* @method
|
|
167
|
+
* @returns {Promise<Array<Object>>} The cross-document audit feed (change-sets, most-recent first).
|
|
168
|
+
* @public
|
|
169
|
+
*/
|
|
170
|
+
listChanges(): Promise<Array<Object>>;
|
|
171
|
+
/**
|
|
172
|
+
* Builds a downloadable snapshot of the current live configuration — for every registered document, its repo file
|
|
173
|
+
* `path` (from registration metadata, if provided), current `version`, and `value`. This is the one-way export
|
|
174
|
+
* *out* of the store; an admin downloads it and commits the files to git. The store remains the live truth.
|
|
175
|
+
*
|
|
176
|
+
* @method
|
|
177
|
+
* @param {Object} [meta]
|
|
178
|
+
* @param {string} [meta.adminID]
|
|
179
|
+
* @returns {Promise<{exportedAt: string, exportedBy: (string|null), documents: Array<{configKey: string, path: (string|null), version: number, value: Object}>}>}
|
|
180
|
+
* @public
|
|
181
|
+
*/
|
|
182
|
+
exportBundle(meta?: {
|
|
183
|
+
adminID?: string;
|
|
184
|
+
}): Promise<{
|
|
185
|
+
exportedAt: string;
|
|
186
|
+
exportedBy: (string | null);
|
|
187
|
+
documents: Array<{
|
|
188
|
+
configKey: string;
|
|
189
|
+
path: (string | null);
|
|
190
|
+
version: number;
|
|
191
|
+
value: Object;
|
|
192
|
+
}>;
|
|
193
|
+
}>;
|
|
194
|
+
/**
|
|
195
|
+
* Seeds a document's default value into the store only if it has never been written (idempotent bootstrap).
|
|
196
|
+
* Used by an application to bring its file defaults into the store at startup before serving live config.
|
|
197
|
+
*
|
|
198
|
+
* @method
|
|
199
|
+
* @param {string} configKey
|
|
200
|
+
* @param {Object} defaultValue
|
|
201
|
+
* @returns {Promise<Object>} The current envelope.
|
|
202
|
+
* @public
|
|
203
|
+
*/
|
|
204
|
+
seedDefault(configKey: string, defaultValue: Object): Promise<Object>;
|
|
205
|
+
/**
|
|
206
|
+
* Subscribes a listener to `config:changed` events (delegates to the change notifier). Returns an unsubscribe fn.
|
|
207
|
+
*
|
|
208
|
+
* @method
|
|
209
|
+
* @param {(configs: Object) => void} listener
|
|
210
|
+
* @returns {() => void}
|
|
211
|
+
* @public
|
|
212
|
+
*/
|
|
213
|
+
onConfigChanged(listener: (configs: Object) => void): () => void;
|
|
214
|
+
}
|
|
215
|
+
declare namespace ConfigService {
|
|
216
|
+
export { instance };
|
|
217
|
+
}
|
|
218
|
+
declare const instance: ConfigService;
|