@jskit-ai/auth-core 0.1.99 → 0.1.101

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.
@@ -0,0 +1,184 @@
1
+ import { normalizeOAuthProviderId } from "./oauthProviders.js";
2
+
3
+ const AUTH_PASSWORD_RECOVERY_DELIVERIES = Object.freeze(["smtp", "dev-log", "dev-response", "disabled"]);
4
+
5
+ const AUTH_OPTIONAL_OPERATION_FEATURES = Object.freeze({
6
+ register: "password.register",
7
+ resendRegisterConfirmation: "emailConfirmation",
8
+ login: "password.login",
9
+ requestOtpLogin: "otp.login",
10
+ verifyOtpLogin: "otp.login",
11
+ oauthStart: "oauthLogin.enabled",
12
+ oauthComplete: "oauthLogin.enabled",
13
+ requestPasswordReset: "passwordRecovery.request",
14
+ completePasswordRecovery: "passwordRecovery.complete",
15
+ resetPassword: "passwordRecovery.complete",
16
+ changePassword: "password.change",
17
+ updateDisplayName: "profileUpdate",
18
+ getSecurityStatus: "securityStatus",
19
+ setPasswordSignInEnabled: "password.methodToggle",
20
+ startProviderLink: "providerLinking.start",
21
+ unlinkProvider: "providerLinking.unlink",
22
+ signOutOtherSessions: "signOutOtherSessions",
23
+ devLoginAs: "devLoginAs"
24
+ });
25
+
26
+ function normalizeBoolean(value, fallback = false) {
27
+ if (typeof value === "boolean") {
28
+ return value;
29
+ }
30
+ return fallback === true;
31
+ }
32
+
33
+ function normalizeAuthProviderId(value, { fallback = "unknown" } = {}) {
34
+ const normalized = String(value || "")
35
+ .trim()
36
+ .toLowerCase();
37
+ if (/^[a-z][a-z0-9_-]{1,63}$/.test(normalized)) {
38
+ return normalized;
39
+ }
40
+
41
+ return fallback;
42
+ }
43
+
44
+ function normalizeProviderLabel(value, providerId) {
45
+ const label = String(value || "").trim();
46
+ if (label) {
47
+ return label;
48
+ }
49
+ return providerId.charAt(0).toUpperCase() + providerId.slice(1);
50
+ }
51
+
52
+ function normalizeOAuthProviderEntries(value) {
53
+ const source = Array.isArray(value) ? value : [];
54
+ const providers = [];
55
+ const seen = new Set();
56
+
57
+ for (const entry of source) {
58
+ const rawId = typeof entry === "string" ? entry : entry?.id;
59
+ const id = normalizeOAuthProviderId(rawId, { fallback: null });
60
+ if (!id || seen.has(id)) {
61
+ continue;
62
+ }
63
+ seen.add(id);
64
+ providers.push(Object.freeze({
65
+ id,
66
+ label: String((typeof entry === "object" && entry ? entry.label : "") || id).trim() || id
67
+ }));
68
+ }
69
+
70
+ return Object.freeze(providers);
71
+ }
72
+
73
+ function normalizeRecoveryDelivery(value, { request = false, complete = false } = {}) {
74
+ const normalized = String(value || "")
75
+ .trim()
76
+ .toLowerCase();
77
+ if (AUTH_PASSWORD_RECOVERY_DELIVERIES.includes(normalized)) {
78
+ return normalized;
79
+ }
80
+ return request || complete ? "disabled" : "disabled";
81
+ }
82
+
83
+ function normalizeAuthCapabilities(value = {}) {
84
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
85
+ const providerSource = source.provider && typeof source.provider === "object" ? source.provider : {};
86
+ const providerId = normalizeAuthProviderId(providerSource.id || source.providerId, { fallback: "unknown" });
87
+ const featuresSource = source.features && typeof source.features === "object" ? source.features : {};
88
+
89
+ const passwordSource = featuresSource.password && typeof featuresSource.password === "object"
90
+ ? featuresSource.password
91
+ : {};
92
+ const recoverySource = featuresSource.passwordRecovery && typeof featuresSource.passwordRecovery === "object"
93
+ ? featuresSource.passwordRecovery
94
+ : {};
95
+ const otpSource = featuresSource.otp && typeof featuresSource.otp === "object"
96
+ ? featuresSource.otp
97
+ : {};
98
+ const oauthSource = featuresSource.oauthLogin && typeof featuresSource.oauthLogin === "object"
99
+ ? featuresSource.oauthLogin
100
+ : {};
101
+ const providerLinkingSource = featuresSource.providerLinking && typeof featuresSource.providerLinking === "object"
102
+ ? featuresSource.providerLinking
103
+ : {};
104
+ const oauthProviders = normalizeOAuthProviderEntries(oauthSource.providers);
105
+ const oauthDefaultProvider = normalizeOAuthProviderId(oauthSource.defaultProvider, { fallback: null });
106
+ const recoveryRequest = normalizeBoolean(recoverySource.request);
107
+ const recoveryComplete = normalizeBoolean(recoverySource.complete);
108
+
109
+ return Object.freeze({
110
+ provider: Object.freeze({
111
+ id: providerId,
112
+ label: normalizeProviderLabel(providerSource.label, providerId)
113
+ }),
114
+ features: Object.freeze({
115
+ password: Object.freeze({
116
+ login: normalizeBoolean(passwordSource.login),
117
+ register: normalizeBoolean(passwordSource.register),
118
+ change: normalizeBoolean(passwordSource.change),
119
+ methodToggle: normalizeBoolean(passwordSource.methodToggle)
120
+ }),
121
+ passwordRecovery: Object.freeze({
122
+ request: recoveryRequest,
123
+ complete: recoveryComplete,
124
+ delivery: normalizeRecoveryDelivery(recoverySource.delivery, {
125
+ request: recoveryRequest,
126
+ complete: recoveryComplete
127
+ })
128
+ }),
129
+ otp: Object.freeze({
130
+ login: normalizeBoolean(otpSource.login)
131
+ }),
132
+ oauthLogin: Object.freeze({
133
+ enabled: normalizeBoolean(oauthSource.enabled) && oauthProviders.length > 0,
134
+ providers: oauthProviders,
135
+ defaultProvider: oauthProviders.some((provider) => provider.id === oauthDefaultProvider)
136
+ ? oauthDefaultProvider
137
+ : null
138
+ }),
139
+ emailConfirmation: normalizeBoolean(featuresSource.emailConfirmation),
140
+ profileUpdate: normalizeBoolean(featuresSource.profileUpdate),
141
+ providerLinking: Object.freeze({
142
+ start: normalizeBoolean(providerLinkingSource.start),
143
+ unlink: normalizeBoolean(providerLinkingSource.unlink)
144
+ }),
145
+ securityStatus: normalizeBoolean(featuresSource.securityStatus),
146
+ signOutOtherSessions: normalizeBoolean(featuresSource.signOutOtherSessions),
147
+ appProfileProjection: normalizeBoolean(featuresSource.appProfileProjection),
148
+ devLoginAs: normalizeBoolean(featuresSource.devLoginAs)
149
+ })
150
+ });
151
+ }
152
+
153
+ function getCapabilityFeature(capabilities, path) {
154
+ const normalizedPath = String(path || "").trim();
155
+ if (!normalizedPath) {
156
+ return undefined;
157
+ }
158
+
159
+ let current = normalizeAuthCapabilities(capabilities).features;
160
+ for (const segment of normalizedPath.split(".")) {
161
+ if (!current || typeof current !== "object" || !Object.hasOwn(current, segment)) {
162
+ return undefined;
163
+ }
164
+ current = current[segment];
165
+ }
166
+ return current;
167
+ }
168
+
169
+ function isAuthOperationSupported(capabilities, operationName) {
170
+ const path = AUTH_OPTIONAL_OPERATION_FEATURES[String(operationName || "").trim()];
171
+ if (!path) {
172
+ return true;
173
+ }
174
+ return getCapabilityFeature(capabilities, path) === true;
175
+ }
176
+
177
+ export {
178
+ AUTH_PASSWORD_RECOVERY_DELIVERIES,
179
+ AUTH_OPTIONAL_OPERATION_FEATURES,
180
+ normalizeAuthProviderId,
181
+ normalizeAuthCapabilities,
182
+ getCapabilityFeature,
183
+ isAuthOperationSupported
184
+ };
@@ -0,0 +1,111 @@
1
+ import { AUTH_METHOD_MINIMUM_ENABLED } from "./authMethods.js";
2
+
3
+ function normalizeBoolean(value, fallback = false) {
4
+ return typeof value === "boolean" ? value : fallback === true;
5
+ }
6
+
7
+ function countEnabledMethods(methods) {
8
+ return methods.filter((method) => method?.enabled === true).length;
9
+ }
10
+
11
+ function normalizeAuthMethodStatus(entry) {
12
+ const source = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
13
+ const id = String(source.id || "").trim();
14
+ const kind = String(source.kind || "").trim();
15
+ if (!id || !kind) {
16
+ return null;
17
+ }
18
+
19
+ const method = {
20
+ id,
21
+ kind,
22
+ configured: normalizeBoolean(source.configured),
23
+ enabled: normalizeBoolean(source.enabled),
24
+ canDisable: normalizeBoolean(source.canDisable)
25
+ };
26
+
27
+ for (const key of [
28
+ "provider",
29
+ "label",
30
+ "canEnable",
31
+ "canUnlink",
32
+ "supportsSecretUpdate",
33
+ "requiresCurrentPassword"
34
+ ]) {
35
+ if (Object.hasOwn(source, key)) {
36
+ method[key] = source[key];
37
+ }
38
+ }
39
+
40
+ return Object.freeze(method);
41
+ }
42
+
43
+ function normalizeSecurityActions(value = {}) {
44
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
45
+ return Object.freeze({
46
+ changePassword: normalizeBoolean(source.changePassword),
47
+ setPasswordEnabled: normalizeBoolean(source.setPasswordEnabled),
48
+ linkProvider: normalizeBoolean(source.linkProvider),
49
+ unlinkProvider: normalizeBoolean(source.unlinkProvider),
50
+ signOutOtherSessions: normalizeBoolean(source.signOutOtherSessions)
51
+ });
52
+ }
53
+
54
+ function normalizeAuthSecurityStatus(value = {}) {
55
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
56
+ const authMethods = (Array.isArray(source.authMethods) ? source.authMethods : [])
57
+ .map((entry) => normalizeAuthMethodStatus(entry))
58
+ .filter(Boolean);
59
+ const policySource =
60
+ source.policy && typeof source.policy === "object"
61
+ ? source.policy
62
+ : source.authPolicy && typeof source.authPolicy === "object"
63
+ ? source.authPolicy
64
+ : {};
65
+ const minimumEnabledMethods = Number.isInteger(Number(policySource.minimumEnabledMethods))
66
+ ? Math.max(1, Number(policySource.minimumEnabledMethods))
67
+ : AUTH_METHOD_MINIMUM_ENABLED;
68
+ const enabledMethodsCount = Number.isInteger(Number(policySource.enabledMethodsCount))
69
+ ? Math.max(0, Number(policySource.enabledMethodsCount))
70
+ : countEnabledMethods(authMethods);
71
+ const policy = Object.freeze({
72
+ minimumEnabledMethods,
73
+ enabledMethodsCount
74
+ });
75
+ const actions = normalizeSecurityActions(source.actions);
76
+
77
+ return Object.freeze({
78
+ mfa: source.mfa && typeof source.mfa === "object"
79
+ ? Object.freeze({
80
+ status: String(source.mfa.status || "not_enabled"),
81
+ enrolled: normalizeBoolean(source.mfa.enrolled),
82
+ methods: Object.freeze(Array.isArray(source.mfa.methods) ? [...source.mfa.methods] : [])
83
+ })
84
+ : Object.freeze({
85
+ status: "not_enabled",
86
+ enrolled: false,
87
+ methods: Object.freeze([])
88
+ }),
89
+ policy,
90
+ authPolicy: policy,
91
+ actions,
92
+ authMethods: Object.freeze(authMethods)
93
+ });
94
+ }
95
+
96
+ function buildSecurityStatusFromAuthMethodsStatus(authMethodsStatus, { actions = {} } = {}) {
97
+ const source = authMethodsStatus && typeof authMethodsStatus === "object" ? authMethodsStatus : {};
98
+ return normalizeAuthSecurityStatus({
99
+ authMethods: Array.isArray(source.methods) ? source.methods : [],
100
+ policy: {
101
+ minimumEnabledMethods: source.minimumEnabledMethods,
102
+ enabledMethodsCount: source.enabledMethodsCount
103
+ },
104
+ actions
105
+ });
106
+ }
107
+
108
+ export {
109
+ normalizeAuthSecurityStatus,
110
+ buildSecurityStatusFromAuthMethodsStatus
111
+ };
@@ -17,6 +17,7 @@ import {
17
17
  AUTH_REFRESH_TOKEN_MAX_LENGTH
18
18
  } from "../authConstraints.js";
19
19
  import { AUTH_METHOD_IDS, AUTH_METHOD_KINDS } from "../authMethods.js";
20
+ import { AUTH_PASSWORD_RECOVERY_DELIVERIES } from "../authCapabilities.js";
20
21
  import { OAUTH_PROVIDER_ID_PATTERN } from "../oauthProviders.js";
21
22
 
22
23
  const oauthProviderFieldDefinition = deepFreeze({
@@ -230,6 +231,109 @@ const authDeniedOutputSchema = createSchema({
230
231
  }
231
232
  });
232
233
 
234
+ const authCapabilityProviderOutputSchema = createSchema({
235
+ id: {
236
+ type: "string",
237
+ required: true,
238
+ minLength: 2,
239
+ maxLength: 64,
240
+ pattern: "^[a-z][a-z0-9_-]{1,63}$"
241
+ },
242
+ label: {
243
+ type: "string",
244
+ required: true,
245
+ minLength: 1,
246
+ maxLength: 120
247
+ }
248
+ });
249
+
250
+ const authPasswordCapabilitiesOutputSchema = createSchema({
251
+ login: { type: "boolean", required: true },
252
+ register: { type: "boolean", required: true },
253
+ change: { type: "boolean", required: true },
254
+ methodToggle: { type: "boolean", required: true }
255
+ });
256
+
257
+ const authPasswordRecoveryCapabilitiesOutputSchema = createSchema({
258
+ request: { type: "boolean", required: true },
259
+ complete: { type: "boolean", required: true },
260
+ delivery: {
261
+ type: "string",
262
+ required: true,
263
+ enum: AUTH_PASSWORD_RECOVERY_DELIVERIES
264
+ }
265
+ });
266
+
267
+ const authOtpCapabilitiesOutputSchema = createSchema({
268
+ login: { type: "boolean", required: true }
269
+ });
270
+
271
+ const authOAuthLoginCapabilitiesOutputSchema = createSchema({
272
+ enabled: { type: "boolean", required: true },
273
+ providers: {
274
+ type: "array",
275
+ required: true,
276
+ items: oauthProviderCatalogEntrySchema
277
+ },
278
+ defaultProvider: {
279
+ ...oauthProviderFieldDefinition,
280
+ required: true,
281
+ nullable: true
282
+ }
283
+ });
284
+
285
+ const authProviderLinkingCapabilitiesOutputSchema = createSchema({
286
+ start: { type: "boolean", required: true },
287
+ unlink: { type: "boolean", required: true }
288
+ });
289
+
290
+ const authCapabilitiesFeaturesOutputSchema = createSchema({
291
+ password: {
292
+ type: "object",
293
+ required: true,
294
+ schema: authPasswordCapabilitiesOutputSchema
295
+ },
296
+ passwordRecovery: {
297
+ type: "object",
298
+ required: true,
299
+ schema: authPasswordRecoveryCapabilitiesOutputSchema
300
+ },
301
+ otp: {
302
+ type: "object",
303
+ required: true,
304
+ schema: authOtpCapabilitiesOutputSchema
305
+ },
306
+ oauthLogin: {
307
+ type: "object",
308
+ required: true,
309
+ schema: authOAuthLoginCapabilitiesOutputSchema
310
+ },
311
+ emailConfirmation: { type: "boolean", required: true },
312
+ profileUpdate: { type: "boolean", required: true },
313
+ providerLinking: {
314
+ type: "object",
315
+ required: true,
316
+ schema: authProviderLinkingCapabilitiesOutputSchema
317
+ },
318
+ securityStatus: { type: "boolean", required: true },
319
+ signOutOtherSessions: { type: "boolean", required: true },
320
+ appProfileProjection: { type: "boolean", required: true },
321
+ devLoginAs: { type: "boolean", required: true }
322
+ });
323
+
324
+ const authCapabilitiesOutputSchema = createSchema({
325
+ provider: {
326
+ type: "object",
327
+ required: true,
328
+ schema: authCapabilityProviderOutputSchema
329
+ },
330
+ features: {
331
+ type: "object",
332
+ required: true,
333
+ schema: authCapabilitiesFeaturesOutputSchema
334
+ }
335
+ });
336
+
233
337
  const sessionOutputSchema = createSchema({
234
338
  authenticated: { type: "boolean", required: true },
235
339
  username: { type: "string", required: false, minLength: 1, maxLength: 120 },
@@ -248,6 +352,11 @@ const sessionOutputSchema = createSchema({
248
352
  required: false,
249
353
  schema: authDeniedOutputSchema
250
354
  },
355
+ authCapabilities: {
356
+ type: "object",
357
+ required: true,
358
+ schema: authCapabilitiesOutputSchema
359
+ },
251
360
  csrfToken: { type: "string", required: true, minLength: 1 },
252
361
  oauthProviders: {
253
362
  type: "array",
@@ -269,6 +378,11 @@ const sessionOutputValidator = deepFreeze({
269
378
  const sessionUnavailableOutputSchema = createSchema({
270
379
  error: { type: "string", required: true, minLength: 1 },
271
380
  csrfToken: { type: "string", required: true, minLength: 1 },
381
+ authCapabilities: {
382
+ type: "object",
383
+ required: true,
384
+ schema: authCapabilitiesOutputSchema
385
+ },
272
386
  oauthProviders: {
273
387
  type: "array",
274
388
  required: true,
@@ -333,6 +447,7 @@ export {
333
447
  logoutOutputValidator,
334
448
  oauthProviderCatalogEntryOutputValidator,
335
449
  authDeniedOutputSchema,
450
+ authCapabilitiesOutputSchema,
336
451
  sessionOutputValidator,
337
452
  sessionUnavailableOutputValidator,
338
453
  createCommandMessages
@@ -8,3 +8,15 @@ export {
8
8
  resolveAuthDeniedLoginMessage
9
9
  } from "./authDenied.js";
10
10
  export { AUTH_PATHS, buildAuthOauthStartPath } from "./authPaths.js";
11
+ export {
12
+ AUTH_PASSWORD_RECOVERY_DELIVERIES,
13
+ AUTH_OPTIONAL_OPERATION_FEATURES,
14
+ normalizeAuthProviderId,
15
+ normalizeAuthCapabilities,
16
+ getCapabilityFeature,
17
+ isAuthOperationSupported
18
+ } from "./authCapabilities.js";
19
+ export {
20
+ normalizeAuthSecurityStatus,
21
+ buildSecurityStatusFromAuthMethodsStatus
22
+ } from "./authSecurityStatus.js";
@@ -0,0 +1,174 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createApplication } from "@jskit-ai/kernel/_testable";
4
+ import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
5
+ import { AuthActionsServiceProvider } from "../src/server/providers/AuthActionsServiceProvider.js";
6
+ import { buildAuthActions } from "../src/server/actions/auth.contributor.js";
7
+
8
+ function createAppConfigFixture() {
9
+ return {
10
+ surfaceModeAll: "all",
11
+ surfaceDefaultId: "home",
12
+ surfaceDefinitions: {
13
+ home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
14
+ console: {
15
+ id: "console",
16
+ pagesRoot: "console",
17
+ enabled: true,
18
+ requiresAuth: true,
19
+ requiresWorkspace: false
20
+ }
21
+ }
22
+ };
23
+ }
24
+
25
+ test("auth logout action delegates to selected auth provider and notifies session changes", async () => {
26
+ const action = buildAuthActions().find((definition) => definition.id === "auth.logout");
27
+ const request = {
28
+ id: "request-1"
29
+ };
30
+ const calls = [];
31
+
32
+ const result = await action.execute(
33
+ {},
34
+ {
35
+ requestMeta: {
36
+ request
37
+ }
38
+ },
39
+ {
40
+ authService: {
41
+ async logout(receivedRequest) {
42
+ calls.push({
43
+ type: "logout",
44
+ request: receivedRequest
45
+ });
46
+ return {
47
+ ok: true,
48
+ clearSession: true
49
+ };
50
+ }
51
+ },
52
+ authSessionEventsService: {
53
+ async notifySessionChanged(payload) {
54
+ calls.push({
55
+ type: "notify",
56
+ context: payload.context
57
+ });
58
+ }
59
+ }
60
+ }
61
+ );
62
+
63
+ assert.deepEqual(result, {
64
+ ok: true,
65
+ clearSession: true
66
+ });
67
+ assert.deepEqual(calls, [
68
+ {
69
+ type: "logout",
70
+ request
71
+ },
72
+ {
73
+ type: "notify",
74
+ context: {
75
+ requestMeta: {
76
+ request
77
+ }
78
+ }
79
+ }
80
+ ]);
81
+ });
82
+
83
+ test("AuthActionsServiceProvider registers shared auth actions against auth.provider", async () => {
84
+ const app = createApplication();
85
+ const logoutCalls = [];
86
+ const published = [];
87
+ class SelectedAuthProvider {
88
+ static id = "auth.provider";
89
+
90
+ register(targetApp) {
91
+ targetApp.singleton("authService", () => ({
92
+ async logout(request) {
93
+ logoutCalls.push(request);
94
+ return {
95
+ ok: true,
96
+ clearSession: true
97
+ };
98
+ },
99
+ async authenticateRequest() {
100
+ return {
101
+ authenticated: false,
102
+ actor: null,
103
+ transientFailure: false
104
+ };
105
+ }
106
+ }));
107
+ }
108
+ }
109
+
110
+ app.instance("appConfig", createAppConfigFixture());
111
+ app.instance("domainEvents", {
112
+ async publish(payload) {
113
+ published.push(payload);
114
+ }
115
+ });
116
+
117
+ await app.start({
118
+ providers: [ActionRuntimeServiceProvider, SelectedAuthProvider, AuthActionsServiceProvider]
119
+ });
120
+
121
+ const actionExecutor = app.make("actionExecutor");
122
+ const definitions = actionExecutor.listDefinitions();
123
+ assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
124
+ assert.equal(definitions.some((definition) => definition.id === "auth.register"), true);
125
+ assert.equal(definitions.some((definition) => definition.id === "auth.dev.loginAs"), false);
126
+ assert.deepEqual(definitions.find((definition) => definition.id === "auth.session.read")?.surfaces, [
127
+ "home",
128
+ "console"
129
+ ]);
130
+
131
+ const request = { id: "request-2" };
132
+ const result = await actionExecutor.execute({
133
+ actionId: "auth.logout",
134
+ input: {},
135
+ context: {
136
+ channel: "internal",
137
+ surface: "home",
138
+ requestMeta: { request },
139
+ actor: { id: 42 }
140
+ }
141
+ });
142
+
143
+ assert.deepEqual(result, {
144
+ ok: true,
145
+ clearSession: true
146
+ });
147
+ assert.deepEqual(logoutCalls, [request]);
148
+ assert.equal(published.length, 2);
149
+ assert.deepEqual(
150
+ published.map((event) => ({
151
+ source: event.source,
152
+ entity: event.entity,
153
+ operation: event.operation,
154
+ entityId: event.entityId,
155
+ realtimeEvent: event.meta?.realtime?.event
156
+ })),
157
+ [
158
+ {
159
+ source: "auth",
160
+ entity: "session",
161
+ operation: "updated",
162
+ entityId: "42",
163
+ realtimeEvent: "auth.session.changed"
164
+ },
165
+ {
166
+ source: "users",
167
+ entity: "bootstrap",
168
+ operation: "updated",
169
+ entityId: "42",
170
+ realtimeEvent: "users.bootstrap.changed"
171
+ }
172
+ ]
173
+ );
174
+ });