ultravisor 1.3.16 → 1.3.17
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/package.json
CHANGED
|
@@ -947,41 +947,92 @@ class UltravisorAPIServer extends libPictService
|
|
|
947
947
|
return { Admitted: false };
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
-
|
|
950
|
+
/**
|
|
951
|
+
* True when this UV is running secured (non-promiscuous) and therefore
|
|
952
|
+
* must require credentials on the protected surface.
|
|
953
|
+
*
|
|
954
|
+
* The authoritative signal is the operator's explicit
|
|
955
|
+
* `UltravisorNonPromiscuous` config flag — the SAME switch that already
|
|
956
|
+
* gates beacon-join admission (_admitBeaconForRegistration). That flag,
|
|
957
|
+
* not the incidental presence of an auth-beacon, is what declares the
|
|
958
|
+
* mesh secured: with it set we fail closed immediately, including during
|
|
959
|
+
* the bootstrap window before the auth-beacon has connected.
|
|
960
|
+
*
|
|
961
|
+
* A connected auth-beacon is also honoured as a safety net so the
|
|
962
|
+
* protected surface is never served unauthenticated merely because the
|
|
963
|
+
* flag was missed — enforcement is thus never weaker than the flag
|
|
964
|
+
* alone. (If you want the flag to be the *sole* signal, drop the bridge
|
|
965
|
+
* check below.)
|
|
966
|
+
*/
|
|
967
|
+
_isSecuredMode()
|
|
968
|
+
{
|
|
969
|
+
let tmpConfig = (this.fable && this.fable.ProgramConfiguration) || {};
|
|
970
|
+
if (tmpConfig.UltravisorNonPromiscuous)
|
|
971
|
+
{
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
let tmpBridge = this._getService('UltravisorAuthBeaconBridge');
|
|
975
|
+
return !!(tmpBridge
|
|
976
|
+
&& typeof tmpBridge.isAvailable === 'function'
|
|
977
|
+
&& tmpBridge.isAvailable());
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Resolve the principal for a request WITHOUT emitting any response.
|
|
982
|
+
*
|
|
983
|
+
* Returns:
|
|
984
|
+
* - a real session object when a valid cookie/token is presented,
|
|
985
|
+
* - a synthetic anonymous session (Anonymous: true) when this UV is
|
|
986
|
+
* running promiscuously (not secured — see _isSecuredMode()),
|
|
987
|
+
* - null when this UV is secured (non-promiscuous) and no valid session
|
|
988
|
+
* was presented (the caller decides how to reject).
|
|
989
|
+
*
|
|
990
|
+
* This is the single source of truth for "who is this request?" -- both
|
|
991
|
+
* the per-route _requireSession() guard and the global
|
|
992
|
+
* _enforceAuthentication() gate flow through it, so the two can never
|
|
993
|
+
* drift. The resolved value is memoized on the request so a request
|
|
994
|
+
* that passes the gate and then hits a route which also calls
|
|
995
|
+
* _requireSession() resolves the session only once.
|
|
996
|
+
*/
|
|
997
|
+
_resolveSession(pRequest)
|
|
951
998
|
{
|
|
952
|
-
|
|
953
|
-
// startup, so the legacy `if (!this._OratorAuth)` escape hatch
|
|
954
|
-
// is dead code in practice -- but it's preserved for any
|
|
955
|
-
// theoretical pre-init request. Treat it the same as the
|
|
956
|
-
// "no auth provider connected" path: hand back an anonymous
|
|
957
|
-
// session rather than 401.
|
|
958
|
-
if (!this._OratorAuth)
|
|
999
|
+
if (pRequest && pRequest._UltravisorSession)
|
|
959
1000
|
{
|
|
960
|
-
return
|
|
1001
|
+
return pRequest._UltravisorSession;
|
|
961
1002
|
}
|
|
962
1003
|
|
|
963
|
-
|
|
1004
|
+
// Resolve the orator-auth session (cookie/token) if one is present.
|
|
1005
|
+
// `_OratorAuth` is wired unconditionally during startup; the guard
|
|
1006
|
+
// is preserved only for a theoretical pre-init request.
|
|
1007
|
+
let tmpResolved = null;
|
|
1008
|
+
let tmpSession = this._OratorAuth ? this._OratorAuth.getSessionForRequest(pRequest) : null;
|
|
964
1009
|
if (tmpSession)
|
|
965
1010
|
{
|
|
966
|
-
|
|
1011
|
+
tmpResolved = tmpSession;
|
|
1012
|
+
}
|
|
1013
|
+
else
|
|
1014
|
+
{
|
|
1015
|
+
// No valid session. In secured (non-promiscuous) mode a missing
|
|
1016
|
+
// session is a hard failure -- return null so the caller 401s,
|
|
1017
|
+
// failing closed even before an auth-beacon has connected. In
|
|
1018
|
+
// promiscuous mode, synthesize an anonymous session so a
|
|
1019
|
+
// beacon-less UV stays genuinely open.
|
|
1020
|
+
tmpResolved = this._isSecuredMode() ? null : this._anonymousSession();
|
|
967
1021
|
}
|
|
968
1022
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
let
|
|
979
|
-
|
|
980
|
-
&& typeof tmpBridge.isAvailable === 'function'
|
|
981
|
-
&& tmpBridge.isAvailable();
|
|
982
|
-
if (!tmpAuthProviderAvailable)
|
|
1023
|
+
if (pRequest && tmpResolved)
|
|
1024
|
+
{
|
|
1025
|
+
pRequest._UltravisorSession = tmpResolved;
|
|
1026
|
+
}
|
|
1027
|
+
return tmpResolved;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
_requireSession(pRequest, pResponse, fNext)
|
|
1031
|
+
{
|
|
1032
|
+
let tmpSession = this._resolveSession(pRequest);
|
|
1033
|
+
if (tmpSession)
|
|
983
1034
|
{
|
|
984
|
-
return
|
|
1035
|
+
return tmpSession;
|
|
985
1036
|
}
|
|
986
1037
|
|
|
987
1038
|
pResponse.send(401, { Error: 'Authentication required.', LoggedIn: false });
|
|
@@ -1006,6 +1057,96 @@ class UltravisorAPIServer extends libPictService
|
|
|
1006
1057
|
};
|
|
1007
1058
|
}
|
|
1008
1059
|
|
|
1060
|
+
/**
|
|
1061
|
+
* Global authentication gate. Registered as an Orator/restify `use`
|
|
1062
|
+
* handler so it runs after routing but before every route's own
|
|
1063
|
+
* handlers, across the entire HTTP surface of this server.
|
|
1064
|
+
*
|
|
1065
|
+
* This is the server-side half of "enforce login when not promiscuous".
|
|
1066
|
+
* Historically only the ~19 beacon worker-protocol routes called
|
|
1067
|
+
* _requireSession(); the whole human-facing management/API surface
|
|
1068
|
+
* (GET /Beacon, /Operation, /Schedule, /Manifest, /Timeline, /Fleet and
|
|
1069
|
+
* their writes) was reachable by any unauthenticated client, with the
|
|
1070
|
+
* web UI as the only gate. This gate closes that.
|
|
1071
|
+
*
|
|
1072
|
+
* Mode-aware, mirroring _resolveSession() / _isSecuredMode():
|
|
1073
|
+
* - Promiscuous (not secured): every request resolves to an anonymous
|
|
1074
|
+
* session and passes -- behaviour identical to before.
|
|
1075
|
+
* - Secured (UltravisorNonPromiscuous set, or an auth-beacon connected):
|
|
1076
|
+
* a valid session is required for everything except the exempt routes
|
|
1077
|
+
* (see _isAuthExemptRoute); anything else gets a 401. Beacons
|
|
1078
|
+
* register and authenticate over WebSocket (which bypasses this HTTP
|
|
1079
|
+
* gate), and their HTTP worker routes already required a session in
|
|
1080
|
+
* secured mode -- so the mesh is unaffected.
|
|
1081
|
+
*/
|
|
1082
|
+
_enforceAuthentication(pRequest, pResponse, fNext)
|
|
1083
|
+
{
|
|
1084
|
+
if (this._isAuthExemptRoute(pRequest))
|
|
1085
|
+
{
|
|
1086
|
+
return fNext();
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Promiscuous -> anonymous session (passes); authenticated with a
|
|
1090
|
+
// valid session -> passes; authenticated without one -> null.
|
|
1091
|
+
let tmpSession = this._resolveSession(pRequest);
|
|
1092
|
+
if (tmpSession)
|
|
1093
|
+
{
|
|
1094
|
+
return fNext();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Authenticated mode, no valid session: reject and stop the chain.
|
|
1098
|
+
// next(false) short-circuits restify's use-chain so the matched
|
|
1099
|
+
// route handler never runs.
|
|
1100
|
+
pResponse.send(401, { Error: 'Authentication required.', LoggedIn: false });
|
|
1101
|
+
return fNext(false);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Routes that must stay reachable without a session even in
|
|
1106
|
+
* authenticated mode -- otherwise the UV would be unusable (you could
|
|
1107
|
+
* never load the login page or sign in). Decided from the routed path
|
|
1108
|
+
* pattern (pRequest.route.path) and the raw request URL:
|
|
1109
|
+
*
|
|
1110
|
+
* - the static web UI ('/*') and the root redirect ('/') -- the
|
|
1111
|
+
* browser must fetch these to render the login screen at all;
|
|
1112
|
+
* - GET /status and /package -- the UI reads these before login to
|
|
1113
|
+
* discover the auth mode (and thus whether to show the login screen);
|
|
1114
|
+
* - POST /Beacon/BootstrapAdmin -- the one-time first-admin bootstrap,
|
|
1115
|
+
* the chicken-and-egg path that necessarily precedes any session;
|
|
1116
|
+
* - the orator-auth namespace ('/1.0/...') -- login, session check,
|
|
1117
|
+
* logout, and the admin-gated /Users CRUD, all of which enforce
|
|
1118
|
+
* their own session/IsAdmin checks downstream.
|
|
1119
|
+
*/
|
|
1120
|
+
_isAuthExemptRoute(pRequest)
|
|
1121
|
+
{
|
|
1122
|
+
let tmpRoutePath = (pRequest && pRequest.route && pRequest.route.path) ? pRequest.route.path : '';
|
|
1123
|
+
|
|
1124
|
+
// Static web UI catch-all + root redirect.
|
|
1125
|
+
if (tmpRoutePath === '/*' || tmpRoutePath === '/')
|
|
1126
|
+
{
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Bootstrap metadata the UI reads before any session exists.
|
|
1131
|
+
if (tmpRoutePath === '/status' || tmpRoutePath === '/package' || tmpRoutePath === '/Beacon/BootstrapAdmin')
|
|
1132
|
+
{
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// orator-auth namespace (login / session / user management). Match
|
|
1137
|
+
// the raw URL prefix too, since these routes are mounted by
|
|
1138
|
+
// orator-auth and we don't want to depend solely on route.path.
|
|
1139
|
+
let tmpUrlPath = (pRequest && pRequest.url) ? pRequest.url : '';
|
|
1140
|
+
let tmpQueryAt = tmpUrlPath.indexOf('?');
|
|
1141
|
+
if (tmpQueryAt >= 0) { tmpUrlPath = tmpUrlPath.slice(0, tmpQueryAt); }
|
|
1142
|
+
if (tmpRoutePath.indexOf('/1.0/') === 0 || tmpUrlPath.indexOf('/1.0/') === 0)
|
|
1143
|
+
{
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1009
1150
|
wireEndpoints(fCallback)
|
|
1010
1151
|
{
|
|
1011
1152
|
if (!this._Orator)
|
|
@@ -1030,28 +1171,26 @@ class UltravisorAPIServer extends libPictService
|
|
|
1030
1171
|
function (pRequest, pResponse, fNext)
|
|
1031
1172
|
{
|
|
1032
1173
|
let tmpHypervisor = this._getService('UltravisorHypervisor');
|
|
1033
|
-
// AuthEnabled
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
// UI components surface this so operators don't
|
|
1040
|
-
// have to infer it from "no auth-beacon connected".
|
|
1174
|
+
// AuthEnabled / AuthMode track whether this UV is secured
|
|
1175
|
+
// (non-promiscuous). Driven by _isSecuredMode() — the
|
|
1176
|
+
// UltravisorNonPromiscuous flag, with a connected
|
|
1177
|
+
// auth-beacon as a safety net — so the UI's login screen
|
|
1178
|
+
// and the server's HTTP auth gate always agree on whether
|
|
1179
|
+
// credentials are required.
|
|
1041
1180
|
let tmpBridge = this._getService('UltravisorAuthBeaconBridge');
|
|
1042
|
-
let
|
|
1181
|
+
let tmpSecured = this._isSecuredMode();
|
|
1182
|
+
// SupportsUserManagement: derived from the auth beacon's
|
|
1183
|
+
// `UserManagement` tag advertised at registration
|
|
1184
|
+
// ('internal' or 'external'). Missing tag defaults to
|
|
1185
|
+
// 'internal' for back-compat. Independent of the secured
|
|
1186
|
+
// flag — it tracks whether an auth-beacon is actually
|
|
1187
|
+
// connected to manage users against, so the UI hides admin
|
|
1188
|
+
// views when there is no beacon.
|
|
1189
|
+
let tmpAuthBeaconPresent = tmpBridge
|
|
1043
1190
|
&& typeof tmpBridge.isAvailable === 'function'
|
|
1044
1191
|
&& tmpBridge.isAvailable();
|
|
1045
|
-
// SupportsUserManagement: derived from the auth
|
|
1046
|
-
// beacon's `UserManagement` tag advertised at
|
|
1047
|
-
// registration ('internal' or 'external'). Missing
|
|
1048
|
-
// tag defaults to 'internal' for back-compat with
|
|
1049
|
-
// older auth beacons that didn't stamp it. In
|
|
1050
|
-
// promiscuous mode the value is reported as `false`
|
|
1051
|
-
// because there's no auth backend to manage users
|
|
1052
|
-
// against — the UI hides admin views accordingly.
|
|
1053
1192
|
let tmpSupportsUM = false;
|
|
1054
|
-
if (
|
|
1193
|
+
if (tmpAuthBeaconPresent && typeof tmpBridge.getAuthBeaconTags === 'function')
|
|
1055
1194
|
{
|
|
1056
1195
|
let tmpTags = tmpBridge.getAuthBeaconTags();
|
|
1057
1196
|
let tmpTag = (tmpTags && tmpTags.UserManagement) || 'internal';
|
|
@@ -1061,8 +1200,8 @@ class UltravisorAPIServer extends libPictService
|
|
|
1061
1200
|
Status: 'Running',
|
|
1062
1201
|
ScheduleEntries: tmpHypervisor ? tmpHypervisor.getSchedule().length : 0,
|
|
1063
1202
|
ScheduleRunning: tmpHypervisor ? tmpHypervisor._Running : false,
|
|
1064
|
-
AuthEnabled: !!
|
|
1065
|
-
AuthMode:
|
|
1203
|
+
AuthEnabled: !!tmpSecured,
|
|
1204
|
+
AuthMode: tmpSecured ? 'authenticated' : 'promiscuous',
|
|
1066
1205
|
SupportsUserManagement: tmpSupportsUM
|
|
1067
1206
|
});
|
|
1068
1207
|
return fNext();
|
|
@@ -3977,6 +4116,28 @@ class UltravisorAPIServer extends libPictService
|
|
|
3977
4116
|
return fNext();
|
|
3978
4117
|
}.bind(this));
|
|
3979
4118
|
|
|
4119
|
+
// Global authentication gate — see _enforceAuthentication(). Registered
|
|
4120
|
+
// as a `use` handler so it covers every routed request (restify's
|
|
4121
|
+
// use-chain is consulted at request time, independent of route
|
|
4122
|
+
// registration order). The handler reads `_OratorAuth` lazily at
|
|
4123
|
+
// request time, so installing it here — before that provider is
|
|
4124
|
+
// instantiated in the next step — is safe: nothing is served until
|
|
4125
|
+
// listen().
|
|
4126
|
+
tmpAnticipate.anticipate(
|
|
4127
|
+
function (fNext)
|
|
4128
|
+
{
|
|
4129
|
+
try
|
|
4130
|
+
{
|
|
4131
|
+
this._OratorServer.server.use(this._enforceAuthentication.bind(this));
|
|
4132
|
+
this.log.info('Ultravisor: authentication gate installed (enforced when an auth-beacon is connected; promiscuous otherwise).');
|
|
4133
|
+
}
|
|
4134
|
+
catch (pErr)
|
|
4135
|
+
{
|
|
4136
|
+
this.log.error('Ultravisor: failed to install authentication gate: ' + (pErr && pErr.message));
|
|
4137
|
+
}
|
|
4138
|
+
return fNext();
|
|
4139
|
+
}.bind(this));
|
|
4140
|
+
|
|
3980
4141
|
tmpAnticipate.anticipate(
|
|
3981
4142
|
function (fNext)
|
|
3982
4143
|
{
|
|
@@ -4128,6 +4289,49 @@ class UltravisorAPIServer extends libPictService
|
|
|
4128
4289
|
// WebSocket Server for Execution Events
|
|
4129
4290
|
// ====================================================================
|
|
4130
4291
|
|
|
4292
|
+
/**
|
|
4293
|
+
* Whether a WebSocket connection may subscribe to consumer event streams
|
|
4294
|
+
* (execution + queue). Those streams expose operational data, so in
|
|
4295
|
+
* secured (non-promiscuous) mode they require a valid session — captured
|
|
4296
|
+
* from the cookie/token on the HTTP upgrade (pWebSocket._Authenticated).
|
|
4297
|
+
* In promiscuous mode every connection may subscribe (unchanged).
|
|
4298
|
+
*
|
|
4299
|
+
* This is the WebSocket half of the HTTP auth gate, keyed off the same
|
|
4300
|
+
* _isSecuredMode(). Beacons are unaffected: they never send
|
|
4301
|
+
* Subscribe/QueueSubscribe — they authenticate at the frame level
|
|
4302
|
+
* (BeaconRegister + join-secret admission), which this does not touch.
|
|
4303
|
+
*/
|
|
4304
|
+
_wsConsumerAuthorized(pWebSocket)
|
|
4305
|
+
{
|
|
4306
|
+
if (!this._isSecuredMode())
|
|
4307
|
+
{
|
|
4308
|
+
return true;
|
|
4309
|
+
}
|
|
4310
|
+
return !!(pWebSocket && pWebSocket._Authenticated);
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
/**
|
|
4314
|
+
* Reject an unauthenticated consumer subscription: emit an auth-required
|
|
4315
|
+
* control frame on the requested stream ('execution' | 'queue'), then
|
|
4316
|
+
* close the socket with 1008 (policy violation) — the WS analogue of the
|
|
4317
|
+
* HTTP gate's 401.
|
|
4318
|
+
*/
|
|
4319
|
+
_rejectWSUnauthenticatedSubscription(pWebSocket, pStream)
|
|
4320
|
+
{
|
|
4321
|
+
try
|
|
4322
|
+
{
|
|
4323
|
+
pWebSocket.send(JSON.stringify(
|
|
4324
|
+
{
|
|
4325
|
+
EventType: (pStream === 'queue') ? 'queue.auth_required' : 'execution.auth_required',
|
|
4326
|
+
Error: 'Authentication required.',
|
|
4327
|
+
LoggedIn: false
|
|
4328
|
+
}));
|
|
4329
|
+
}
|
|
4330
|
+
catch (pErr) { /* socket already dying */ }
|
|
4331
|
+
try { pWebSocket.close(1008, 'Authentication required'); }
|
|
4332
|
+
catch (pErr) { /* ignore */ }
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4131
4335
|
/**
|
|
4132
4336
|
* Initialize a WebSocket server on the same HTTP server used by Restify.
|
|
4133
4337
|
* Clients connect and subscribe to a RunHash to receive real-time
|
|
@@ -4187,6 +4391,17 @@ class UltravisorAPIServer extends libPictService
|
|
|
4187
4391
|
}
|
|
4188
4392
|
catch (pErr) { /* best-effort — never block the upgrade on auth lookup */ }
|
|
4189
4393
|
pWebSocket._UpgradeSessionID = (tmpUpgradeSession && tmpUpgradeSession.SessionID) || null;
|
|
4394
|
+
// Pin whether a *valid* session (real cookie/token, not
|
|
4395
|
+
// anonymous) was presented on the upgrade. The WS
|
|
4396
|
+
// consumer gate (_wsConsumerAuthorized) uses this to
|
|
4397
|
+
// require credentials for event-stream subscriptions when
|
|
4398
|
+
// the UV is secured. We never block the upgrade itself —
|
|
4399
|
+
// beacons authenticate at the frame level (BeaconRegister
|
|
4400
|
+
// + join-secret admission), so gating must be per-frame.
|
|
4401
|
+
pWebSocket._Authenticated = !!(tmpUpgradeSession
|
|
4402
|
+
&& tmpUpgradeSession.SessionID
|
|
4403
|
+
&& tmpUpgradeSession.SessionID !== 'anonymous'
|
|
4404
|
+
&& !tmpUpgradeSession.Anonymous);
|
|
4190
4405
|
this._WebSocketServer.emit('connection', pWebSocket, pRequest);
|
|
4191
4406
|
}.bind(this));
|
|
4192
4407
|
}.bind(this));
|
|
@@ -4232,6 +4447,14 @@ class UltravisorAPIServer extends libPictService
|
|
|
4232
4447
|
|
|
4233
4448
|
if (tmpData.Action === 'Subscribe' && tmpData.RunHash)
|
|
4234
4449
|
{
|
|
4450
|
+
// Auth gate: in secured mode an execution-event
|
|
4451
|
+
// subscription requires a valid session on the
|
|
4452
|
+
// connection. Beacons never send this frame.
|
|
4453
|
+
if (!this._wsConsumerAuthorized(pWebSocket))
|
|
4454
|
+
{
|
|
4455
|
+
this._rejectWSUnauthenticatedSubscription(pWebSocket, 'execution');
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
4235
4458
|
// --- Execution event subscription (web UI) ---
|
|
4236
4459
|
//
|
|
4237
4460
|
// Mirrors the QueueSubscribe resync protocol:
|
|
@@ -4326,6 +4549,14 @@ class UltravisorAPIServer extends libPictService
|
|
|
4326
4549
|
}
|
|
4327
4550
|
else if (tmpData.Action === 'QueueSubscribe')
|
|
4328
4551
|
{
|
|
4552
|
+
// Auth gate: in secured mode a queue-event
|
|
4553
|
+
// subscription requires a valid session on the
|
|
4554
|
+
// connection. Beacons never send this frame.
|
|
4555
|
+
if (!this._wsConsumerAuthorized(pWebSocket))
|
|
4556
|
+
{
|
|
4557
|
+
this._rejectWSUnauthenticatedSubscription(pWebSocket, 'queue');
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4329
4560
|
// QueueSubscribe doubles as a resync request:
|
|
4330
4561
|
// callers that pass LastEventGUID get a
|
|
4331
4562
|
// replay block (replay_begin / events after
|