@xenon-device-management/xenon 2.15.0 → 2.16.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/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +26 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +14 -6
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +32 -18
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +5 -8
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +11 -2
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +1 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
2
18
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
19
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
20
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
21
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
22
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
23
|
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
42
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
43
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -19,9 +52,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
19
52
|
};
|
|
20
53
|
var SocketServer_1;
|
|
21
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.SocketServer = void 0;
|
|
55
|
+
exports.SocketServer = exports.SOCKET_RECHECK_INTERVAL_MS = void 0;
|
|
23
56
|
const socket_io_1 = require("socket.io");
|
|
24
57
|
const typedi_1 = require("typedi");
|
|
58
|
+
const jose = __importStar(require("jose"));
|
|
25
59
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
60
|
const config_1 = require("../config");
|
|
27
61
|
const ApiKeyService_1 = require("./ApiKeyService");
|
|
@@ -34,13 +68,39 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
|
34
68
|
const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
|
|
35
69
|
const upgradeRouter_1 = require("../app/ws/upgradeRouter");
|
|
36
70
|
const sessionCommandSummary_1 = require("../dashboard/sessionCommandSummary");
|
|
71
|
+
const SelectorVisibilityResolver_1 = require("./selector-health/SelectorVisibilityResolver");
|
|
72
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
37
73
|
/** socket.io's default path, which the dashboard and nodes connect to. */
|
|
38
74
|
const SOCKET_IO_PATH = '/socket.io';
|
|
75
|
+
/**
|
|
76
|
+
* How often every socket is checked again, for a change this server didn't
|
|
77
|
+
* make itself (another server sharing the database, a direct edit). Changes
|
|
78
|
+
* made through Xenon reach a socket at once (onIdentityChanged).
|
|
79
|
+
*/
|
|
80
|
+
exports.SOCKET_RECHECK_INTERVAL_MS = 60000;
|
|
81
|
+
/** setTimeout fires at once for a longer delay. */
|
|
82
|
+
const MAX_TIMER_MS = 2147483647;
|
|
83
|
+
/** How many sockets a sweep checks at once, so a large lab's sweep is no burst of queries. */
|
|
84
|
+
const SWEEP_BATCH = 10;
|
|
85
|
+
/**
|
|
86
|
+
* The credential no longer admits the socket: wrong, revoked, expired, or its
|
|
87
|
+
* user inactive or gone. Anything else thrown while checking means the check
|
|
88
|
+
* could not run (the database or signing key unavailable).
|
|
89
|
+
*/
|
|
90
|
+
class SocketRefused extends Error {
|
|
91
|
+
}
|
|
39
92
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
40
93
|
const EVENT_LOG_KEEPS = new Map([
|
|
41
94
|
[SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, null],
|
|
42
95
|
[SocketEvents_1.SocketEvents.SESSION_COMMAND, sessionCommandSummary_1.sessionCommandSummary],
|
|
43
96
|
]);
|
|
97
|
+
/** A row's expiresAt as epoch ms, or undefined for none. */
|
|
98
|
+
function msOf(at) {
|
|
99
|
+
if (!at)
|
|
100
|
+
return undefined;
|
|
101
|
+
const ms = new Date(at).getTime();
|
|
102
|
+
return Number.isNaN(ms) ? undefined : ms;
|
|
103
|
+
}
|
|
44
104
|
function readCookie(cookieHeader, name) {
|
|
45
105
|
if (!cookieHeader)
|
|
46
106
|
return undefined;
|
|
@@ -60,8 +120,23 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
60
120
|
this.nodes = new Map(); // socketId -> nodeHost
|
|
61
121
|
/** The tail of each phone's pending event deliveries (see emitToDashboardForDevices). */
|
|
62
122
|
this.deliveries = new Map();
|
|
123
|
+
/** The tail of each selector's pending event deliveries (see emitToDashboardForSelector). */
|
|
124
|
+
this.selectorDeliveries = new Map();
|
|
125
|
+
/** The timer that checks a socket again when its credential ends, by socket id. */
|
|
126
|
+
this.expiryTimers = new Map();
|
|
127
|
+
/**
|
|
128
|
+
* Each socket's checks, numbered as they start. A check's outcome is
|
|
129
|
+
* applied unless one that started after it has already applied its own.
|
|
130
|
+
*/
|
|
131
|
+
this.checks = new WeakMap();
|
|
132
|
+
/** The check of a socket whose handshake overlapped a change; it joins the dashboard room after it. */
|
|
133
|
+
this.connectChecks = new WeakMap();
|
|
134
|
+
/** Counts the identity changes, so a handshake that overlapped one checks again. */
|
|
135
|
+
this.identityEpoch = 0;
|
|
136
|
+
this.sweeping = false;
|
|
63
137
|
}
|
|
64
|
-
initialize(server) {
|
|
138
|
+
initialize(server, options = {}) {
|
|
139
|
+
var _a;
|
|
65
140
|
// socket.io (engine.io) takes its websocket transport by adding its own
|
|
66
141
|
// `upgrade` listener, which would also see every other upgrade and end
|
|
67
142
|
// the ones it doesn't own after 1 s. The upgrade router moves that
|
|
@@ -79,9 +154,12 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
79
154
|
}));
|
|
80
155
|
this.io.use((socket, next) => __awaiter(this, void 0, void 0, function* () {
|
|
81
156
|
try {
|
|
82
|
-
const
|
|
157
|
+
const epoch = this.identityEpoch;
|
|
158
|
+
const { identity, endsAt } = yield this.check(socket);
|
|
83
159
|
socket.data.identity = identity;
|
|
84
160
|
socket.data.principal = identity.principal;
|
|
161
|
+
socket.data.endsAt = endsAt;
|
|
162
|
+
socket.data.checkedEpoch = epoch;
|
|
85
163
|
next();
|
|
86
164
|
}
|
|
87
165
|
catch (err) {
|
|
@@ -93,6 +171,13 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
93
171
|
const socketId = socket.id;
|
|
94
172
|
const principal = socket.data.principal;
|
|
95
173
|
logger_1.default.info(`[SocketServer] New connection: ${socketId} (principal=${principal})`);
|
|
174
|
+
this.armExpiry(socket);
|
|
175
|
+
// A user changed while this handshake was being checked. The change
|
|
176
|
+
// found no socket of theirs to check yet, so this one may hold what was
|
|
177
|
+
// true before it.
|
|
178
|
+
if (socket.data.checkedEpoch !== this.identityEpoch) {
|
|
179
|
+
this.connectChecks.set(socket, this.recheck(socket));
|
|
180
|
+
}
|
|
96
181
|
// Protocol version handshake — still runs after auth so a logged-in
|
|
97
182
|
// client on the wrong protocol version still gets a clean disconnect.
|
|
98
183
|
socket.on(SocketEvents_1.SocketEvents.HANDSHAKE, (data) => {
|
|
@@ -117,16 +202,22 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
117
202
|
// Notify dashboard about new node
|
|
118
203
|
this.emitToDashboard(SocketEvents_1.SocketEvents.NODE_CONNECTED, { host });
|
|
119
204
|
});
|
|
120
|
-
socket.on(SocketEvents_1.SocketEvents.REGISTER_DASHBOARD, () => {
|
|
205
|
+
socket.on(SocketEvents_1.SocketEvents.REGISTER_DASHBOARD, () => __awaiter(this, void 0, void 0, function* () {
|
|
121
206
|
if (!SocketServer_1.canRegisterAs(principal, 'dashboard')) {
|
|
122
207
|
logger_1.default.warn(`[SocketServer] Rejected REGISTER_DASHBOARD from principal=${principal} on ${socketId}`);
|
|
123
208
|
socket.disconnect();
|
|
124
209
|
return;
|
|
125
210
|
}
|
|
211
|
+
// Its events wait for the identity the overlapping change gave it.
|
|
212
|
+
yield this.connectChecks.get(socket);
|
|
213
|
+
if (!socket.connected)
|
|
214
|
+
return;
|
|
126
215
|
logger_1.default.info(`[SocketServer] Dashboard client registered (Socket: ${socketId})`);
|
|
127
216
|
socket.join('dashboard');
|
|
128
|
-
});
|
|
217
|
+
}));
|
|
129
218
|
socket.on('disconnect', () => {
|
|
219
|
+
clearTimeout(this.expiryTimers.get(socketId));
|
|
220
|
+
this.expiryTimers.delete(socketId);
|
|
130
221
|
if (this.nodes.has(socketId)) {
|
|
131
222
|
const host = this.nodes.get(socketId);
|
|
132
223
|
logger_1.default.info(`[SocketServer] Node disconnected: ${host} (Socket: ${socketId})`);
|
|
@@ -138,8 +229,128 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
138
229
|
}
|
|
139
230
|
});
|
|
140
231
|
});
|
|
232
|
+
const stopListening = (0, identityChanges_1.onIdentityChanged)((userId) => this.recheckUser(userId));
|
|
233
|
+
const sweep = setInterval(() => void this.sweep(), (_a = options.recheckIntervalMs) !== null && _a !== void 0 ? _a : exports.SOCKET_RECHECK_INTERVAL_MS);
|
|
234
|
+
sweep.unref();
|
|
235
|
+
server.once('close', () => {
|
|
236
|
+
stopListening();
|
|
237
|
+
clearInterval(sweep);
|
|
238
|
+
});
|
|
141
239
|
logger_1.default.info('[SocketServer] WebSocket server initialized (auth enabled)');
|
|
142
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Checks every socket of `userId` again, on its own credential (see
|
|
243
|
+
* recheck). Settles once each has its new identity or is closed (or, if it
|
|
244
|
+
* could not be checked, is as it was).
|
|
245
|
+
*/
|
|
246
|
+
recheckUser(userId) {
|
|
247
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
248
|
+
this.identityEpoch += 1;
|
|
249
|
+
if (!this.io || config_1.config.authDisabled === true)
|
|
250
|
+
return;
|
|
251
|
+
const sockets = Array.from(this.io.sockets.sockets.values()).filter((socket) => { var _a, _b; return ((_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.userId) === userId; });
|
|
252
|
+
yield Promise.all(sockets.map((socket) => this.recheck(socket)));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Every socket again, for a change made where this server doesn't hear of
|
|
257
|
+
* it, SWEEP_BATCH at a time. One sweep at a time.
|
|
258
|
+
*/
|
|
259
|
+
sweep() {
|
|
260
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
if (this.sweeping || !this.io || config_1.config.authDisabled === true)
|
|
262
|
+
return;
|
|
263
|
+
this.sweeping = true;
|
|
264
|
+
try {
|
|
265
|
+
const sockets = Array.from(this.io.sockets.sockets.values());
|
|
266
|
+
for (let i = 0; i < sockets.length; i += SWEEP_BATCH) {
|
|
267
|
+
yield Promise.all(sockets.slice(i, i + SWEEP_BATCH).map((s) => this.recheck(s)));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
this.sweeping = false;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The handshake's check, again, on the socket's own credential, renewing
|
|
277
|
+
* nothing: a sign-in's TTL doesn't slide and a key's `lastUsedAt` doesn't
|
|
278
|
+
* move. Its outcome is applied unless a check that started after it has
|
|
279
|
+
* already applied its own. So once a check settles, the socket is at least
|
|
280
|
+
* as current as what it read: a change's check lands before REST answers,
|
|
281
|
+
* though a sweep's started meanwhile, and a newer check that could not run
|
|
282
|
+
* doesn't cancel an older refusal.
|
|
283
|
+
*
|
|
284
|
+
* - Admitted: the socket gets its new identity where it is, still
|
|
285
|
+
* connected, so a demoted admin's tab keeps its member's events.
|
|
286
|
+
* - Refused: its connection is closed (the transport, not
|
|
287
|
+
* `disconnect()`). The client reconnects as it does after any drop, and
|
|
288
|
+
* the handshake decides on the credential it has now: a fresh bearer
|
|
289
|
+
* token from its `auth` function (Xenon Studio, the documented client),
|
|
290
|
+
* or the browser's new cookie, is admitted; the refused one is refused,
|
|
291
|
+
* and socket.io-client doesn't try again after a refused handshake.
|
|
292
|
+
* `disconnect()` would have the client never reconnect at all
|
|
293
|
+
* (`io server disconnect`), so every token's `exp` would end a
|
|
294
|
+
* long-running client's events for good. Once closing, the socket is
|
|
295
|
+
* sent nothing more.
|
|
296
|
+
* - Could not check: the socket is kept as it was. Closing every
|
|
297
|
+
* dashboard over a database hiccup would have their reconnects refused
|
|
298
|
+
* too, leaving them all without live events.
|
|
299
|
+
*/
|
|
300
|
+
recheck(socket) {
|
|
301
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
var _a;
|
|
303
|
+
let state = this.checks.get(socket);
|
|
304
|
+
if (!state) {
|
|
305
|
+
state = { started: 0, applied: 0 };
|
|
306
|
+
this.checks.set(socket, state);
|
|
307
|
+
}
|
|
308
|
+
const checks = state;
|
|
309
|
+
const n = ++checks.started;
|
|
310
|
+
const current = () => n > checks.applied && socket.connected;
|
|
311
|
+
const before = socket.data.identity;
|
|
312
|
+
let checked;
|
|
313
|
+
try {
|
|
314
|
+
checked = yield this.check(socket, true);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
if (!current())
|
|
318
|
+
return;
|
|
319
|
+
if (err instanceof SocketRefused) {
|
|
320
|
+
checks.applied = n;
|
|
321
|
+
logger_1.default.info(`[SocketServer] Closing ${socket.id} (user ${before === null || before === void 0 ? void 0 : before.userId}): ${err.message}`);
|
|
322
|
+
socket.conn.close();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
logger_1.default.warn(`[SocketServer] Could not check ${socket.id} (user ${before === null || before === void 0 ? void 0 : before.userId}) again, so it is kept as it was: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!current())
|
|
330
|
+
return;
|
|
331
|
+
checks.applied = n;
|
|
332
|
+
const { identity, endsAt } = checked;
|
|
333
|
+
socket.data.identity = identity;
|
|
334
|
+
socket.data.endsAt = endsAt;
|
|
335
|
+
this.armExpiry(socket);
|
|
336
|
+
if ((before === null || before === void 0 ? void 0 : before.role) !== identity.role ||
|
|
337
|
+
JSON.stringify(before === null || before === void 0 ? void 0 : before.teamIds) !== JSON.stringify(identity.teamIds)) {
|
|
338
|
+
logger_1.default.info(`[SocketServer] ${socket.id} (user ${identity.userId}) is now ${identity.role}, teams ${identity.teamIds ? `[${identity.teamIds.join(', ')}]` : 'all'}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** Checks the socket again when its credential ends by itself (`socket.data.endsAt`). */
|
|
343
|
+
armExpiry(socket) {
|
|
344
|
+
clearTimeout(this.expiryTimers.get(socket.id));
|
|
345
|
+
this.expiryTimers.delete(socket.id);
|
|
346
|
+
const endsAt = socket.data.endsAt;
|
|
347
|
+
if (endsAt === undefined || !socket.connected)
|
|
348
|
+
return;
|
|
349
|
+
const delay = Math.min(Math.max(endsAt - Date.now(), 0), MAX_TIMER_MS);
|
|
350
|
+
const timer = setTimeout(() => void this.recheck(socket), delay);
|
|
351
|
+
timer.unref();
|
|
352
|
+
this.expiryTimers.set(socket.id, timer);
|
|
353
|
+
}
|
|
143
354
|
// Role-based registration guard. A dashboard-authed socket can't join the
|
|
144
355
|
// 'nodes' broadcast room and a node-authed socket can't subscribe to
|
|
145
356
|
// dashboard events — keeps a stolen credential of one class from acting
|
|
@@ -151,13 +362,27 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
151
362
|
}
|
|
152
363
|
authenticate(socket) {
|
|
153
364
|
return __awaiter(this, void 0, void 0, function* () {
|
|
154
|
-
|
|
365
|
+
return (yield this.check(socket)).identity;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Who the socket's handshake credential says it is now, and when that
|
|
370
|
+
* credential ends by itself (`endsAt`, epoch ms; none for one that doesn't).
|
|
371
|
+
* Throws SocketRefused when the credential no longer admits it, anything
|
|
372
|
+
* else when it could not be checked. A `recheck` renews nothing (see
|
|
373
|
+
* recheck).
|
|
374
|
+
*/
|
|
375
|
+
check(socket_1) {
|
|
376
|
+
return __awaiter(this, arguments, void 0, function* (socket, recheck = false) {
|
|
377
|
+
var _a, _b, _c, _d, _e;
|
|
155
378
|
if (config_1.config.authDisabled === true) {
|
|
156
379
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
380
|
+
identity: {
|
|
381
|
+
principal: 'auth-disabled',
|
|
382
|
+
userId: 'auth-disabled',
|
|
383
|
+
role: 'SUPER_ADMIN',
|
|
384
|
+
teamIds: undefined,
|
|
385
|
+
},
|
|
161
386
|
};
|
|
162
387
|
}
|
|
163
388
|
const auth = (socket.handshake.auth || {});
|
|
@@ -171,58 +396,74 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
171
396
|
try {
|
|
172
397
|
payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(bearer, { audience: 'xenon-rest' });
|
|
173
398
|
}
|
|
174
|
-
catch (
|
|
175
|
-
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (err instanceof jose.errors.JOSEError)
|
|
401
|
+
throw new SocketRefused('invalid bearer token');
|
|
402
|
+
throw new Error(`bearer token could not be checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
176
403
|
}
|
|
404
|
+
// The socket closes when REST stops taking the token: verify() allows
|
|
405
|
+
// JWT_CLOCK_TOLERANCE_SEC past exp, and refuses it from then on.
|
|
406
|
+
const endsAt = typeof payload.exp === 'number'
|
|
407
|
+
? (payload.exp + JwtKeyService_1.JWT_CLOCK_TOLERANCE_SEC) * 1000
|
|
408
|
+
: undefined;
|
|
177
409
|
const owner = yield prisma_1.prisma.user.findUnique({
|
|
178
410
|
where: { id: String(payload.sub) },
|
|
179
411
|
select: { status: true, role: true },
|
|
180
412
|
});
|
|
181
413
|
if (!owner || owner.status !== 'ACTIVE')
|
|
182
|
-
throw new
|
|
414
|
+
throw new SocketRefused('inactive user');
|
|
183
415
|
// The team claim is read exactly as REST's bearer path reads it.
|
|
184
|
-
|
|
416
|
+
const identity = yield this.identify('dashboard', String(payload.sub), owner.role, (_b = payload.teamId) !== null && _b !== void 0 ? _b : null);
|
|
417
|
+
return { identity, endsAt };
|
|
185
418
|
}
|
|
186
419
|
// Node path: per-node (accessKey, token) pair. Resolves to a real
|
|
187
420
|
// ApiKey row owned by a real User; we additionally enforce that the
|
|
188
421
|
// User is ACTIVE so a disabled account can't keep a stolen key alive.
|
|
189
422
|
const pairKey = (typeof auth.accessKey === 'string' && auth.accessKey) ||
|
|
190
|
-
((
|
|
423
|
+
((_c = headers['x-xenon-access-key']) !== null && _c !== void 0 ? _c : '');
|
|
191
424
|
const pairTok = (typeof auth.token === 'string' && auth.token) ||
|
|
192
|
-
((
|
|
425
|
+
((_d = headers['x-xenon-token']) !== null && _d !== void 0 ? _d : '');
|
|
193
426
|
if (pairKey && pairTok) {
|
|
194
|
-
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair(pairKey, pairTok
|
|
427
|
+
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair(pairKey, pairTok, {
|
|
428
|
+
touch: !recheck,
|
|
429
|
+
});
|
|
195
430
|
if (!row)
|
|
196
|
-
throw new
|
|
431
|
+
throw new SocketRefused('invalid (accessKey, token) pair');
|
|
197
432
|
const owner = yield prisma_1.prisma.user.findUnique({
|
|
198
433
|
where: { id: row.userId },
|
|
199
434
|
select: { status: true, role: true },
|
|
200
435
|
});
|
|
201
436
|
if (!owner || owner.status !== 'ACTIVE')
|
|
202
|
-
throw new
|
|
203
|
-
|
|
437
|
+
throw new SocketRefused('inactive user');
|
|
438
|
+
const identity = yield this.identify('node', row.userId, owner.role, row.teamId);
|
|
439
|
+
return { identity, endsAt: msOf(row.expiresAt) };
|
|
204
440
|
}
|
|
205
441
|
// Dashboard path: the cookie. As in REST's authMiddleware, it is first a
|
|
206
442
|
// UserSession id (set by /auth/login), then a raw API key (the older
|
|
207
443
|
// /api-key-gate cookie). A session whose user is disabled is refused, not
|
|
208
444
|
// retried as a key.
|
|
209
|
-
const cookieValue = (
|
|
445
|
+
const cookieValue = (_e = readCookie(headers.cookie, SESSION_COOKIE)) !== null && _e !== void 0 ? _e : '';
|
|
210
446
|
if (!cookieValue) {
|
|
211
|
-
throw new
|
|
447
|
+
throw new SocketRefused('missing credentials (need (accessKey, token) pair or dashboard cookie)');
|
|
212
448
|
}
|
|
213
|
-
|
|
449
|
+
// A sign-in's expiresAt slides as REST uses it: checked again then, it
|
|
450
|
+
// has moved on and the timer is set again.
|
|
451
|
+
const session = yield typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve(cookieValue, {
|
|
452
|
+
renew: !recheck,
|
|
453
|
+
});
|
|
214
454
|
if (session) {
|
|
215
455
|
const user = yield prisma_1.prisma.user.findUnique({
|
|
216
456
|
where: { id: session.userId },
|
|
217
457
|
select: { status: true, role: true },
|
|
218
458
|
});
|
|
219
459
|
if (!user || user.status !== 'ACTIVE')
|
|
220
|
-
throw new
|
|
221
|
-
|
|
460
|
+
throw new SocketRefused('inactive user');
|
|
461
|
+
const identity = yield this.identify('dashboard', session.userId, user.role, null);
|
|
462
|
+
return { identity, endsAt: msOf(session.expiresAt) };
|
|
222
463
|
}
|
|
223
|
-
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verify(cookieValue);
|
|
464
|
+
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verify(cookieValue, { touch: !recheck });
|
|
224
465
|
if (!row) {
|
|
225
|
-
throw new
|
|
466
|
+
throw new SocketRefused('invalid or revoked dashboard session');
|
|
226
467
|
}
|
|
227
468
|
// The key owner's role decides the team scope, and an inactive owner is
|
|
228
469
|
// refused here as REST refuses them.
|
|
@@ -231,8 +472,9 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
231
472
|
select: { status: true, role: true },
|
|
232
473
|
});
|
|
233
474
|
if (!owner || owner.status !== 'ACTIVE')
|
|
234
|
-
throw new
|
|
235
|
-
|
|
475
|
+
throw new SocketRefused('inactive user');
|
|
476
|
+
const identity = yield this.identify('dashboard', row.userId, owner.role, row.teamId);
|
|
477
|
+
return { identity, endsAt: msOf(row.expiresAt) };
|
|
236
478
|
});
|
|
237
479
|
}
|
|
238
480
|
identify(principal, userId, role, apiKeyTeamId) {
|
|
@@ -242,7 +484,10 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
242
484
|
return { principal, userId, role: r, teamIds };
|
|
243
485
|
});
|
|
244
486
|
}
|
|
245
|
-
/**
|
|
487
|
+
/**
|
|
488
|
+
* Unscoped: every dashboard socket. Only for the hub's node events, which
|
|
489
|
+
* carry a node's address and nothing of a phone, a session or a selector.
|
|
490
|
+
*/
|
|
246
491
|
emitToDashboard(event, data) {
|
|
247
492
|
if (this.io) {
|
|
248
493
|
this.io.to('dashboard').emit(event, data);
|
|
@@ -259,6 +504,55 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
259
504
|
payload: keep ? keep(data) : data,
|
|
260
505
|
});
|
|
261
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* A Selector Health event (`selector_*`): each dashboard socket gets it only
|
|
509
|
+
* if its caller may see the selector, by REST's rule (a selector healed in a
|
|
510
|
+
* session the caller may see; SelectorVisibilityResolver). Admins get every
|
|
511
|
+
* one. The event log records it once, unscoped.
|
|
512
|
+
*
|
|
513
|
+
* Each selector's events are delivered through one chain, in the order they
|
|
514
|
+
* were emitted. With no scoped socket connected and nothing pending for the
|
|
515
|
+
* selector, it is the synchronous room broadcast, with no lookup. The
|
|
516
|
+
* returned promise settles once the event is delivered and never rejects.
|
|
517
|
+
*/
|
|
518
|
+
emitToDashboardForSelector(event, data, selector) {
|
|
519
|
+
var _a;
|
|
520
|
+
this.logEvent(event, data);
|
|
521
|
+
const key = JSON.stringify([selector.strategy, selector.selector]);
|
|
522
|
+
if (!this.hasScopedDashboard() && !this.selectorDeliveries.has(key)) {
|
|
523
|
+
(_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
|
|
524
|
+
return Promise.resolve();
|
|
525
|
+
}
|
|
526
|
+
return this.enqueue(event, [key], () => this.deliverForSelector(event, data, selector), this.selectorDeliveries);
|
|
527
|
+
}
|
|
528
|
+
deliverForSelector(event, data, selector) {
|
|
529
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
530
|
+
var _a;
|
|
531
|
+
// The scoped sockets may have left while this waited its turn.
|
|
532
|
+
if (!this.hasScopedDashboard()) {
|
|
533
|
+
(_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const resolver = typedi_1.Container.get(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
|
|
537
|
+
const sockets = this.dashboardSockets();
|
|
538
|
+
const asked = sockets.map((socket) => { var _a; return (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity; });
|
|
539
|
+
const allowed = yield Promise.all(sockets.map((socket) => {
|
|
540
|
+
var _a, _b;
|
|
541
|
+
const teamIds = teamIdsOf(socket);
|
|
542
|
+
if (teamIds === undefined)
|
|
543
|
+
return true;
|
|
544
|
+
const userId = (_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.userId;
|
|
545
|
+
return resolver.canSee(selector, { userId, teamIds });
|
|
546
|
+
}));
|
|
547
|
+
// A socket whose identity was replaced meanwhile (a role or team
|
|
548
|
+
// change) was not the one asked about: it gets nothing.
|
|
549
|
+
sockets.forEach((socket, i) => {
|
|
550
|
+
var _a;
|
|
551
|
+
if (allowed[i] && ((_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === asked[i])
|
|
552
|
+
socket.emit(event, data);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|
|
262
556
|
/**
|
|
263
557
|
* A dashboard event about one or more phones: each dashboard socket gets it
|
|
264
558
|
* only if its caller can see the phone ({@link DeviceEventScope}), by the
|
|
@@ -280,7 +574,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
280
574
|
var _a, _b;
|
|
281
575
|
this.logEvent(event, data);
|
|
282
576
|
const udids = 'udids' in scope ? scope.udids : [(_a = scope.udid) !== null && _a !== void 0 ? _a : ''];
|
|
283
|
-
if (
|
|
577
|
+
if (this.reachesEverySocket(scope) && udids.every((udid) => !this.deliveries.has(udid))) {
|
|
284
578
|
this.noteRow(scope);
|
|
285
579
|
(_b = this.io) === null || _b === void 0 ? void 0 : _b.to('dashboard').emit(event, data);
|
|
286
580
|
return Promise.resolve();
|
|
@@ -305,34 +599,53 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
305
599
|
hasScopedDashboard() {
|
|
306
600
|
return this.dashboardSockets().some((socket) => teamIdsOf(socket) !== undefined);
|
|
307
601
|
}
|
|
308
|
-
/** Runs `step` after every pending delivery for `
|
|
309
|
-
enqueue(event,
|
|
310
|
-
const before =
|
|
602
|
+
/** Runs `step` after every pending delivery for `keys` in `chains`, and makes it their new tail. */
|
|
603
|
+
enqueue(event, keys, step, chains = this.deliveries) {
|
|
604
|
+
const before = keys.map((key) => { var _a; return (_a = chains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
|
|
311
605
|
const done = Promise.all(before)
|
|
312
606
|
.then(step)
|
|
313
607
|
.catch((err) => { var _a; return logger_1.default.warn(`[SocketServer] ${event} not delivered: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
|
|
314
|
-
for (const
|
|
315
|
-
|
|
608
|
+
for (const key of keys)
|
|
609
|
+
chains.set(key, done);
|
|
316
610
|
void done.then(() => {
|
|
317
|
-
for (const
|
|
318
|
-
if (
|
|
319
|
-
|
|
611
|
+
for (const key of keys) {
|
|
612
|
+
if (chains.get(key) === done)
|
|
613
|
+
chains.delete(key);
|
|
320
614
|
}
|
|
321
615
|
});
|
|
322
616
|
return done;
|
|
323
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Whether every dashboard socket gets an event of this scope whatever its
|
|
620
|
+
* phones are, so it can be the room broadcast with no lookup.
|
|
621
|
+
*/
|
|
622
|
+
reachesEverySocket(scope) {
|
|
623
|
+
return this.dashboardSockets().every((socket) => teamIdsOf(socket) === undefined && (!scope.adminOnly || isAdmin(socket)));
|
|
624
|
+
}
|
|
324
625
|
deliver(event, data, scope) {
|
|
325
626
|
return __awaiter(this, void 0, void 0, function* () {
|
|
326
627
|
var _a;
|
|
327
628
|
// The scoped sockets may have left while this waited its turn.
|
|
328
|
-
if (
|
|
629
|
+
if (this.reachesEverySocket(scope)) {
|
|
329
630
|
(_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
|
|
330
631
|
return;
|
|
331
632
|
}
|
|
332
633
|
const resolver = typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver);
|
|
634
|
+
// Read once the team is known, so a socket that joined meanwhile is included.
|
|
635
|
+
const recipients = () => this.dashboardSockets().filter((socket) => !scope.adminOnly || isAdmin(socket));
|
|
636
|
+
// An admin-only event goes to admins, who see every phone: no lookup, so
|
|
637
|
+
// a slow store holds no captured request back.
|
|
638
|
+
if (scope.adminOnly) {
|
|
639
|
+
const admins = recipients();
|
|
640
|
+
if (admins.every((socket) => teamIdsOf(socket) === undefined)) {
|
|
641
|
+
for (const socket of admins)
|
|
642
|
+
socket.emit(event, data);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
333
646
|
if ('udids' in scope) {
|
|
334
647
|
const teams = yield Promise.all(scope.udids.map((udid) => resolver.resolve(udid)));
|
|
335
|
-
for (const socket of
|
|
648
|
+
for (const socket of recipients()) {
|
|
336
649
|
const teamIds = teamIdsOf(socket);
|
|
337
650
|
if (teamIds === undefined) {
|
|
338
651
|
socket.emit(event, data);
|
|
@@ -348,7 +661,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
348
661
|
const team = scope.teamId !== undefined
|
|
349
662
|
? { known: true, teamId: scope.teamId }
|
|
350
663
|
: yield resolver.resolve(scope.udid);
|
|
351
|
-
for (const socket of
|
|
664
|
+
for (const socket of recipients()) {
|
|
352
665
|
if ((0, DeviceTeamResolver_1.canSeeDeviceTeam)(team, teamIdsOf(socket)))
|
|
353
666
|
socket.emit(event, data);
|
|
354
667
|
}
|
|
@@ -384,3 +697,9 @@ function teamIdsOf(socket) {
|
|
|
384
697
|
const identity = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity;
|
|
385
698
|
return identity ? identity.teamIds : [];
|
|
386
699
|
}
|
|
700
|
+
/** An admin by role, as `roleGuard('ADMIN')` decides. A socket with no identity is not one. */
|
|
701
|
+
function isAdmin(socket) {
|
|
702
|
+
var _a, _b;
|
|
703
|
+
const role = (_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.role;
|
|
704
|
+
return role === 'ADMIN' || role === 'SUPER_ADMIN';
|
|
705
|
+
}
|
|
@@ -22,6 +22,7 @@ exports.TeamService = void 0;
|
|
|
22
22
|
const typedi_1 = require("typedi");
|
|
23
23
|
const prisma_1 = require("../prisma");
|
|
24
24
|
const logger_1 = __importDefault(require("../logger"));
|
|
25
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
25
26
|
let TeamService = class TeamService {
|
|
26
27
|
constructor() {
|
|
27
28
|
this.log = logger_1.default.scope('Team');
|
|
@@ -101,7 +102,9 @@ let TeamService = class TeamService {
|
|
|
101
102
|
}
|
|
102
103
|
addMember(teamId, userId) {
|
|
103
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
-
|
|
105
|
+
const member = yield prisma_1.prisma.teamMember.create({ data: { teamId, userId } });
|
|
106
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
107
|
+
return member;
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
removeMember(teamId, userId) {
|
|
@@ -109,6 +112,7 @@ let TeamService = class TeamService {
|
|
|
109
112
|
yield prisma_1.prisma.teamMember.delete({
|
|
110
113
|
where: { teamId_userId: { teamId, userId } },
|
|
111
114
|
});
|
|
115
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
112
116
|
});
|
|
113
117
|
}
|
|
114
118
|
};
|