@xenon-device-management/xenon 2.15.0 → 2.17.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-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.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/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- 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 +23 -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 +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- 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 +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- 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 +52 -23
- 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/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- 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 +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -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 +74 -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/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- 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/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -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 +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -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-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -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
|
@@ -14,42 +14,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const node_child_process_1 = require("node:child_process");
|
|
16
16
|
const config_1 = require("../config");
|
|
17
|
-
const prepare_prisma_1 = require("./prepare-prisma");
|
|
18
17
|
const logger_1 = __importDefault(require("../logger"));
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
const database_check_1 = require("./database-check");
|
|
19
|
+
const run_migrations_1 = require("./run-migrations");
|
|
20
|
+
/**
|
|
21
|
+
* `npm run db:migrate`: bring the database up to date, then regenerate the
|
|
22
|
+
* client.
|
|
23
|
+
*
|
|
24
|
+
* It checks the database URL and uses the rule the server uses at startup
|
|
25
|
+
* (database-check.ts, then run-migrations.ts): the database decides between
|
|
26
|
+
* `migrate deploy` and `db push`, so a database whose migration history
|
|
27
|
+
* matches its tables stays on `migrate deploy`. It runs whatever
|
|
28
|
+
* XENON_AUTO_MIGRATE says, since it is how you update the schema when that is
|
|
29
|
+
* off. Through 2.15.0 it always ran `db push`, and first rewrote
|
|
30
|
+
* prisma/schema.prisma's provider to `databaseProvider`. With `postgresql`,
|
|
31
|
+
* that left a PostgreSQL schema in the checkout that the SQLite file couldn't
|
|
32
|
+
* use.
|
|
33
|
+
*/
|
|
34
34
|
function main() {
|
|
35
35
|
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (config_1.config.databaseProvider === 'sqlite') {
|
|
41
|
-
logger_1.default.info('[DBInit] Syncing SQLite schema via db push...');
|
|
42
|
-
executeCmd('npx prisma db push --accept-data-loss --skip-generate');
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
// For PostgreSQL, we might not have migrations checked in yet.
|
|
46
|
-
// In "Cellular Architecture", we use db push to ensure the schema is synced
|
|
47
|
-
// without requiring migration history sync across cells.
|
|
48
|
-
logger_1.default.info('[DBInit] Syncing PostgreSQL schema via db push...');
|
|
49
|
-
executeCmd('npx prisma db push --accept-data-loss');
|
|
50
|
-
}
|
|
36
|
+
// A PostgreSQL URL would otherwise reach the history read and fail there,
|
|
37
|
+
// under a message about the file.
|
|
38
|
+
(0, database_check_1.assertSupportedDatabase)();
|
|
39
|
+
yield (0, run_migrations_1.syncDatabaseSchema)();
|
|
51
40
|
logger_1.default.info('[DBInit] Generating Prisma Client...');
|
|
52
|
-
|
|
41
|
+
(0, node_child_process_1.execSync)('npx prisma generate', {
|
|
42
|
+
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: config_1.config.databaseUrl }),
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
});
|
|
53
45
|
});
|
|
54
46
|
}
|
|
55
|
-
(() =>
|
|
47
|
+
main().catch((err) => {
|
|
48
|
+
var _a;
|
|
49
|
+
logger_1.default.error(`[DBInit] ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
@@ -45,6 +45,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.planSchemaSync = planSchemaSync;
|
|
49
|
+
exports.readMigrationHistory = readMigrationHistory;
|
|
50
|
+
exports.schemaSyncFailure = schemaSyncFailure;
|
|
51
|
+
exports.syncDatabaseSchema = syncDatabaseSchema;
|
|
48
52
|
exports.runMigrations = runMigrations;
|
|
49
53
|
const node_child_process_1 = require("node:child_process");
|
|
50
54
|
const fs = __importStar(require("node:fs"));
|
|
@@ -52,6 +56,230 @@ const path = __importStar(require("node:path"));
|
|
|
52
56
|
const os = __importStar(require("node:os"));
|
|
53
57
|
const logger_1 = __importDefault(require("../logger"));
|
|
54
58
|
const config_1 = require("../config");
|
|
59
|
+
const database_check_1 = require("./database-check");
|
|
60
|
+
/**
|
|
61
|
+
* Which command brings a database up to date. `historyMatchesTables` is asked
|
|
62
|
+
* only about a database with migrations left to apply. Given the applied
|
|
63
|
+
* migrations, it says whether the tables are what they make, or null when it
|
|
64
|
+
* can't tell.
|
|
65
|
+
*/
|
|
66
|
+
function planSchemaSync(history, localMigrations, historyMatchesTables) {
|
|
67
|
+
if (!history) {
|
|
68
|
+
return {
|
|
69
|
+
command: 'db push',
|
|
70
|
+
acceptDataLoss: true,
|
|
71
|
+
reason: 'it has no migration history (a new database, or one kept up to date with db push)',
|
|
72
|
+
basis: 'no-history',
|
|
73
|
+
failed: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (history.failed.length > 0) {
|
|
77
|
+
return {
|
|
78
|
+
command: 'db push',
|
|
79
|
+
acceptDataLoss: false,
|
|
80
|
+
reason: `its migration history records a failed migration (${history.failed.join(', ')}), ` +
|
|
81
|
+
'which migrate deploy will not get past',
|
|
82
|
+
basis: 'failed-migration',
|
|
83
|
+
failed: history.failed,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const applied = new Set(history.applied);
|
|
87
|
+
const pending = localMigrations.filter((name) => !applied.has(name));
|
|
88
|
+
if (pending.length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
command: 'migrate deploy',
|
|
91
|
+
acceptDataLoss: false,
|
|
92
|
+
reason: 'its migration history has every migration',
|
|
93
|
+
basis: 'complete',
|
|
94
|
+
failed: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const matches = historyMatchesTables(localMigrations.filter((name) => applied.has(name)));
|
|
98
|
+
if (matches === false) {
|
|
99
|
+
return {
|
|
100
|
+
command: 'db push',
|
|
101
|
+
acceptDataLoss: false,
|
|
102
|
+
reason: 'its tables differ from what its recorded migrations make (db push updated it, or it ' +
|
|
103
|
+
'was changed by hand), so migrate deploy could fail on a change that is already there',
|
|
104
|
+
basis: 'tables-differ',
|
|
105
|
+
failed: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
command: 'migrate deploy',
|
|
110
|
+
acceptDataLoss: false,
|
|
111
|
+
reason: `its migration history is missing ${pending.length} migration(s)` +
|
|
112
|
+
(matches === null ? ' (the tables could not be compared with the history)' : ''),
|
|
113
|
+
basis: matches === null ? 'uncompared' : 'behind',
|
|
114
|
+
failed: [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The file a SQLite `file:` URL names, or null for anything else. Prisma reads
|
|
119
|
+
* a relative path from the schema's directory, so that is `baseDir`; without
|
|
120
|
+
* one, a relative path gives null.
|
|
121
|
+
*/
|
|
122
|
+
function sqliteFilePath(databaseUrl, baseDir) {
|
|
123
|
+
if (!databaseUrl.startsWith('file:'))
|
|
124
|
+
return null;
|
|
125
|
+
const file = databaseUrl.slice('file:'.length).split('?')[0];
|
|
126
|
+
if (path.isAbsolute(file))
|
|
127
|
+
return file;
|
|
128
|
+
return baseDir ? path.resolve(baseDir, file) : null;
|
|
129
|
+
}
|
|
130
|
+
/** The migration history in `_prisma_migrations`, or null when the database has none. */
|
|
131
|
+
function readMigrationHistory(databaseUrl) {
|
|
132
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
var _a, _b, _c;
|
|
134
|
+
const file = sqliteFilePath(databaseUrl);
|
|
135
|
+
if (file && !fs.existsSync(file))
|
|
136
|
+
return null;
|
|
137
|
+
const { PrismaClient } = yield Promise.resolve().then(() => __importStar(require('../generated/client')));
|
|
138
|
+
const client = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
|
|
139
|
+
try {
|
|
140
|
+
const rows = yield client.$queryRawUnsafe('SELECT migration_name, finished_at, rolled_back_at FROM "_prisma_migrations"');
|
|
141
|
+
return {
|
|
142
|
+
applied: rows.filter((r) => r.finished_at && !r.rolled_back_at).map((r) => r.migration_name),
|
|
143
|
+
failed: rows.filter((r) => !r.finished_at && !r.rolled_back_at).map((r) => r.migration_name),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const message = String((_c = (_b = (_a = err === null || err === void 0 ? void 0 : err.meta) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : err);
|
|
148
|
+
if (/no such table|relation .* does not exist/i.test(message))
|
|
149
|
+
return null;
|
|
150
|
+
if (/no such column|column .* does not exist/i.test(message)) {
|
|
151
|
+
// A table of that name that isn't Prisma's. The default setting always
|
|
152
|
+
// started on such a database, with db push, which leaves the table alone.
|
|
153
|
+
logger_1.default.warn("[DBMigrate] The database's _prisma_migrations table is not Prisma's migration " +
|
|
154
|
+
`history (${message}), so Xenon treats the database as having none.`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
yield client.$disconnect();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** The migrations in prisma/migrations, in the order Prisma applies them. */
|
|
165
|
+
function localMigrationsIn(migrationsDir) {
|
|
166
|
+
return fs
|
|
167
|
+
.readdirSync(migrationsDir)
|
|
168
|
+
.filter((name) => fs.existsSync(path.join(migrationsDir, name, 'migration.sql')))
|
|
169
|
+
.sort();
|
|
170
|
+
}
|
|
171
|
+
/** A word a POSIX shell reads as itself: quoted only when it has to be. */
|
|
172
|
+
function shellWord(word) {
|
|
173
|
+
return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, "'\\''")}'`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The commands a failure's message gives, ready to paste. A copy is made with
|
|
177
|
+
* SQLite's `VACUUM INTO` through `prisma db execute`, so it is consistent
|
|
178
|
+
* whatever journal sits beside the file, and needs nothing but the CLI.
|
|
179
|
+
*/
|
|
180
|
+
function pasteable(at) {
|
|
181
|
+
const schema = shellWord(at.schema);
|
|
182
|
+
const url = (file) => `file:${file}`;
|
|
183
|
+
return {
|
|
184
|
+
copyTo(target) {
|
|
185
|
+
const statement = `VACUUM INTO '${target.replace(/'/g, "''")}'`;
|
|
186
|
+
const quoted = /["$`\\!]/.test(statement) ? shellWord(statement) : `"${statement}"`;
|
|
187
|
+
return `echo ${quoted} | ${at.prisma} db execute --url ${shellWord(at.databaseUrl)} --stdin`;
|
|
188
|
+
},
|
|
189
|
+
deploy(file) {
|
|
190
|
+
return `DATABASE_URL=${shellWord(url(file))} ${at.prisma} migrate deploy --schema ${schema}`;
|
|
191
|
+
},
|
|
192
|
+
letGo() {
|
|
193
|
+
return (`DATABASE_URL=${shellWord(at.databaseUrl)} ${at.prisma} db push --skip-generate ` +
|
|
194
|
+
`--accept-data-loss --schema ${schema}`);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** A backup's name: the file's, stamped with the time, so one never overwrites another. */
|
|
199
|
+
function backupOf(file, now) {
|
|
200
|
+
const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
|
|
201
|
+
return `${file}.backup-${stamp}`;
|
|
202
|
+
}
|
|
203
|
+
/** The migration a P3018 names, if it does. */
|
|
204
|
+
function failedMigrationIn(output) {
|
|
205
|
+
const match = /Migration name:\s*(\S+)/.exec(output);
|
|
206
|
+
return match ? match[1] : null;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* What went wrong and what to do, then Prisma's own output. A refusal to
|
|
210
|
+
* delete data gives the commands to run, with this server's paths, and never
|
|
211
|
+
* sends `migrate deploy` to a database it would fail on: one with a failed
|
|
212
|
+
* migration recorded (P3009), or one whose tables are ahead of its history
|
|
213
|
+
* (P3018, which records the migration as failed).
|
|
214
|
+
*/
|
|
215
|
+
function schemaSyncFailure(plan, output, at, now = new Date()) {
|
|
216
|
+
const where = (0, database_check_1.describeDatabaseUrl)(at.databaseUrl);
|
|
217
|
+
const run = pasteable(at);
|
|
218
|
+
let sentence;
|
|
219
|
+
if (/Added the required column/i.test(output)) {
|
|
220
|
+
sentence =
|
|
221
|
+
`This version adds a required column to a table that already has rows in ${where}, ` +
|
|
222
|
+
'which prisma db push cannot do: back the file up, then migrate it by hand or start ' +
|
|
223
|
+
'from a fresh database.';
|
|
224
|
+
}
|
|
225
|
+
else if (/accept-data-loss|There might be data loss/i.test(output)) {
|
|
226
|
+
const why = plan.basis === 'failed-migration'
|
|
227
|
+
? `its migration history records a failed migration (${plan.failed.join(', ')}), ` +
|
|
228
|
+
"which prisma migrate deploy won't run past"
|
|
229
|
+
: 'its tables differ from what its recorded migrations make (something was added to ' +
|
|
230
|
+
'them by hand, or prisma db push moved them past the history)';
|
|
231
|
+
const backup = at.file
|
|
232
|
+
? `Back the file up first:\n ${run.copyTo(backupOf(at.file, now))}\n`
|
|
233
|
+
: 'Back the database up first. ';
|
|
234
|
+
sentence =
|
|
235
|
+
'prisma db push stopped rather than delete what Prisma lists below from the database ' +
|
|
236
|
+
`at ${where}. Xenon brings this database up to date with prisma db push because ${why}, ` +
|
|
237
|
+
'and without --accept-data-loss db push deletes no table or column that holds data. ' +
|
|
238
|
+
backup +
|
|
239
|
+
`To let what is listed go, run this and start again:\n ${run.letGo()}\n`;
|
|
240
|
+
if (plan.basis === 'failed-migration' || !at.file) {
|
|
241
|
+
sentence += 'To keep any of it, copy it out of the database first.';
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// migrate deploy works on the file only if the tables are not ahead of
|
|
245
|
+
// its history, which nothing here can tell: a copy finds out.
|
|
246
|
+
const copy = `${at.file}.copy`;
|
|
247
|
+
sentence +=
|
|
248
|
+
"To keep it, try this version's migrations on a copy of the file instead:\n" +
|
|
249
|
+
` ${run.copyTo(copy)}\n` +
|
|
250
|
+
` ${run.deploy(copy)}\n` +
|
|
251
|
+
'If that works, run them on the file, delete the copy and start again:\n' +
|
|
252
|
+
` ${run.deploy(at.file)}\n` +
|
|
253
|
+
'If it fails, they would fail on the file too (P3018 means the tables already have ' +
|
|
254
|
+
"a change the history doesn't record): delete the copy, copy out anything you need, " +
|
|
255
|
+
'and let what is listed go as above.';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (/P3018/.test(output)) {
|
|
259
|
+
const name = failedMigrationIn(output);
|
|
260
|
+
sentence =
|
|
261
|
+
`A migration${name ? `, ${name},` : ''} failed on the database at ${where}, and its ` +
|
|
262
|
+
"migration history now records it as failed. Check Prisma's reason below. If what the " +
|
|
263
|
+
'migration makes is already there ("already exists", "duplicate column name"), start ' +
|
|
264
|
+
'again: Xenon brings a database with a failed migration recorded up to date with ' +
|
|
265
|
+
'prisma db push, which stops before deleting a table or column that holds data. If ' +
|
|
266
|
+
'the migration failed on the rows in the database (a unique index over duplicate ' +
|
|
267
|
+
'values, say), back the file up, change those rows, and start again.';
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
sentence =
|
|
271
|
+
`prisma ${plan.command} could not bring the database at ${where} up to date: back the ` +
|
|
272
|
+
'file up and fix what Prisma reports below, or, if its schema already matches this ' +
|
|
273
|
+
'version, set XENON_AUTO_MIGRATE=false to start without changing it.';
|
|
274
|
+
}
|
|
275
|
+
return `[DBMigrate] Cannot start: ${sentence}\n\n${output}`;
|
|
276
|
+
}
|
|
277
|
+
/** What a failed prisma call printed: its warnings (stdout) and its error (stderr). */
|
|
278
|
+
function cliOutput(err) {
|
|
279
|
+
var _a;
|
|
280
|
+
const parts = [err === null || err === void 0 ? void 0 : err.stdout, err === null || err === void 0 ? void 0 : err.stderr].map((b) => (b ? b.toString().trim() : ''));
|
|
281
|
+
return parts.filter(Boolean).join('\n') || String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err).trim();
|
|
282
|
+
}
|
|
55
283
|
function resolvePluginRoot() {
|
|
56
284
|
let dir = __dirname;
|
|
57
285
|
for (let i = 0; i < 8; i++) {
|
|
@@ -81,15 +309,14 @@ function resolvePrismaInvocation(rootDir) {
|
|
|
81
309
|
return { cmd: localBin, prefix: [] };
|
|
82
310
|
return { cmd: 'npx', prefix: ['prisma'] };
|
|
83
311
|
}
|
|
84
|
-
|
|
85
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Bring the database at `config.databaseUrl` up to date with prisma/schema.prisma,
|
|
314
|
+
* by the rule above. Throws an Error whose message says what to do when it can't.
|
|
315
|
+
* `npm run db:migrate` runs this too, whatever XENON_AUTO_MIGRATE says.
|
|
316
|
+
*/
|
|
317
|
+
function syncDatabaseSchema() {
|
|
318
|
+
return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory) {
|
|
86
319
|
var _a;
|
|
87
|
-
if (!config_1.config.autoMigrate) {
|
|
88
|
-
logger_1.default.info('[DBMigrate] Auto-migrate disabled by XENON_AUTO_MIGRATE=false. ' +
|
|
89
|
-
'Apply schema changes externally (`prisma migrate deploy` for ' +
|
|
90
|
-
'PostgreSQL, `prisma db push` for SQLite) before the hub boots.');
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
320
|
const rootDir = resolvePluginRoot();
|
|
94
321
|
const schemaPath = path.join(rootDir, 'prisma', 'schema.prisma');
|
|
95
322
|
const migrationsDir = path.join(rootDir, 'prisma', 'migrations');
|
|
@@ -97,33 +324,101 @@ function runMigrations() {
|
|
|
97
324
|
logger_1.default.warn(`[DBMigrate] Skipping auto-migrate: schema or migrations missing at ${rootDir}`);
|
|
98
325
|
return;
|
|
99
326
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
327
|
+
const databaseUrl = config_1.config.databaseUrl;
|
|
328
|
+
const where = (0, database_check_1.describeDatabaseUrl)(databaseUrl);
|
|
329
|
+
// The file the CLI will use: a relative path is read from prisma/, as Prisma does.
|
|
330
|
+
const file = sqliteFilePath(databaseUrl, path.dirname(schemaPath));
|
|
331
|
+
fs.mkdirSync(path.dirname(file !== null && file !== void 0 ? file : config_1.config.databasePath), { recursive: true });
|
|
102
332
|
const { cmd, prefix } = resolvePrismaInvocation(rootDir);
|
|
103
|
-
const env = Object.assign(Object.assign({}, process.env), { DATABASE_URL:
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
333
|
+
const env = Object.assign(Object.assign({}, process.env), { DATABASE_URL: databaseUrl });
|
|
334
|
+
const opts = { env, cwd: rootDir, stdio: 'pipe' };
|
|
335
|
+
let history;
|
|
336
|
+
try {
|
|
337
|
+
history = yield readHistory(file ? `file:${file}` : databaseUrl);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
throw new Error('[DBMigrate] Cannot start: Xenon could not read the migration history of the database ' +
|
|
341
|
+
`at ${where}, so it can't tell how to bring it up to date: if another server is using ` +
|
|
342
|
+
'the file, stop that one; otherwise back the file up, check that it is a Xenon SQLite ' +
|
|
343
|
+
`database (Prisma's message is below), and start again.\n\n${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
344
|
+
}
|
|
345
|
+
// Would the history's own migrations make exactly these tables? Built from
|
|
346
|
+
// this release's copies of the applied migrations, in a scratch directory.
|
|
347
|
+
const historyMatchesTables = (applied) => {
|
|
348
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-migration-history-'));
|
|
349
|
+
try {
|
|
350
|
+
const lock = path.join(migrationsDir, 'migration_lock.toml');
|
|
351
|
+
if (fs.existsSync(lock))
|
|
352
|
+
fs.copyFileSync(lock, path.join(scratch, 'migration_lock.toml'));
|
|
353
|
+
for (const name of applied) {
|
|
354
|
+
fs.mkdirSync(path.join(scratch, name));
|
|
355
|
+
fs.copyFileSync(path.join(migrationsDir, name, 'migration.sql'), path.join(scratch, name, 'migration.sql'));
|
|
356
|
+
}
|
|
357
|
+
const diff = ['migrate', 'diff', '--from-migrations', scratch, '--to-schema-datasource'];
|
|
358
|
+
runSync(cmd, [...prefix, ...diff, schemaPath, '--exit-code'], opts);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
if ((err === null || err === void 0 ? void 0 : err.status) === 2)
|
|
363
|
+
return false;
|
|
364
|
+
logger_1.default.warn(`[DBMigrate] Could not compare the tables with the migration history: ${cliOutput(err)}`);
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const plan = planSchemaSync(history, localMigrationsIn(migrationsDir), historyMatchesTables);
|
|
372
|
+
const announce = `[DBMigrate] Syncing database schema (${plan.command}) at ${where}: ${plan.reason}.`;
|
|
373
|
+
// A database with a history that leaves migrate deploy is worth a look.
|
|
374
|
+
if (history && plan.command === 'db push')
|
|
375
|
+
logger_1.default.warn(announce);
|
|
376
|
+
else
|
|
377
|
+
logger_1.default.info(announce);
|
|
378
|
+
const args = plan.command === 'db push'
|
|
379
|
+
? [
|
|
380
|
+
...prefix,
|
|
381
|
+
'db',
|
|
382
|
+
'push',
|
|
383
|
+
'--skip-generate',
|
|
384
|
+
...(plan.acceptDataLoss ? ['--accept-data-loss'] : []),
|
|
385
|
+
'--schema',
|
|
386
|
+
schemaPath,
|
|
387
|
+
]
|
|
107
388
|
: [...prefix, 'migrate', 'deploy', '--schema', schemaPath];
|
|
108
389
|
try {
|
|
109
|
-
|
|
110
|
-
runSync(cmd, args, { env, cwd: rootDir, stdio: 'pipe' });
|
|
390
|
+
runSync(cmd, args, opts);
|
|
111
391
|
logger_1.default.info('[DBMigrate] Database schema in sync.');
|
|
112
392
|
}
|
|
113
393
|
catch (err) {
|
|
114
|
-
const msg = (
|
|
115
|
-
logger_1.default.error(`[DBMigrate]
|
|
394
|
+
const msg = cliOutput(err);
|
|
395
|
+
logger_1.default.error(`[DBMigrate] prisma ${plan.command} failed: ${msg}`);
|
|
116
396
|
// Boot must not continue. The schema is not what the code expects, so the
|
|
117
397
|
// next query fails somewhere unrelated — `prisma.user.count()` reporting a
|
|
118
398
|
// missing User table, say — and buries this, the actual cause. Stop here
|
|
119
399
|
// and hand back something the operator can act on.
|
|
120
|
-
throw new Error(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
400
|
+
throw new Error(schemaSyncFailure(plan, msg, {
|
|
401
|
+
prisma: [cmd, ...prefix].map(shellWord).join(' '),
|
|
402
|
+
schema: schemaPath,
|
|
403
|
+
databaseUrl: file ? `file:${file}` : databaseUrl,
|
|
404
|
+
file,
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/** The startup step: `syncDatabaseSchema`, unless XENON_AUTO_MIGRATE=false. */
|
|
410
|
+
function runMigrations() {
|
|
411
|
+
return __awaiter(this, arguments, void 0, function* (runSync = node_child_process_1.execFileSync, readHistory = readMigrationHistory) {
|
|
412
|
+
if (!config_1.config.autoMigrate) {
|
|
413
|
+
logger_1.default.info('[DBMigrate] Auto-migrate disabled by XENON_AUTO_MIGRATE=false. Bring the schema up ' +
|
|
414
|
+
'to date yourself before the server starts: `prisma migrate deploy` for a database ' +
|
|
415
|
+
'whose migration history (`_prisma_migrations`) matches its tables (`prisma migrate ' +
|
|
416
|
+
'diff --from-migrations <the recorded ones> --to-schema-datasource prisma/schema.prisma ' +
|
|
417
|
+
'--exit-code` says so), `prisma db push` for any other (a `migrate deploy` that stops ' +
|
|
418
|
+
'with P3018 on a change already there means the database is one of those), or ' +
|
|
419
|
+
'`npm run db:migrate` from a source checkout, which chooses for you.');
|
|
420
|
+
return;
|
|
127
421
|
}
|
|
422
|
+
yield syncDatabaseSchema(runSync, readHistory);
|
|
128
423
|
});
|
|
129
424
|
}
|
|
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const crypto_1 = __importDefault(require("crypto"));
|
|
24
24
|
const prisma_1 = require("../prisma");
|
|
25
25
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
26
27
|
let ApiKeyService = class ApiKeyService {
|
|
27
28
|
constructor() {
|
|
28
29
|
this.log = logger_1.default.scope('ApiKey');
|
|
@@ -38,8 +39,13 @@ let ApiKeyService = class ApiKeyService {
|
|
|
38
39
|
generateRaw() {
|
|
39
40
|
return crypto_1.default.randomBytes(32).toString('hex');
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The live key for `raw`. A use of it is recorded (`lastUsedAt`);
|
|
44
|
+
* `touch: false` only looks, for a check that isn't a use (a socket's
|
|
45
|
+
* re-check). Likewise verifyPair.
|
|
46
|
+
*/
|
|
47
|
+
verify(raw_1) {
|
|
48
|
+
return __awaiter(this, arguments, void 0, function* (raw, { touch = true } = {}) {
|
|
43
49
|
if (!raw)
|
|
44
50
|
return null;
|
|
45
51
|
const row = yield prisma_1.prisma.apiKey.findUnique({ where: { keyHash: this.hash(raw) } });
|
|
@@ -47,6 +53,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
47
53
|
return null;
|
|
48
54
|
if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
|
|
49
55
|
return null;
|
|
56
|
+
if (!touch)
|
|
57
|
+
return row;
|
|
50
58
|
prisma_1.prisma.apiKey
|
|
51
59
|
.update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
|
|
52
60
|
.catch(() => undefined);
|
|
@@ -77,8 +85,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
77
85
|
return { id: row.id, raw };
|
|
78
86
|
});
|
|
79
87
|
}
|
|
80
|
-
verifyPair(
|
|
81
|
-
return __awaiter(this,
|
|
88
|
+
verifyPair(accessKey_1, token_1) {
|
|
89
|
+
return __awaiter(this, arguments, void 0, function* (accessKey, token, { touch = true } = {}) {
|
|
82
90
|
if (!accessKey || !token)
|
|
83
91
|
return null;
|
|
84
92
|
const user = yield prisma_1.prisma.user.findUnique({ where: { accessKey } });
|
|
@@ -91,6 +99,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
91
99
|
return null;
|
|
92
100
|
if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
|
|
93
101
|
return null;
|
|
102
|
+
if (!touch)
|
|
103
|
+
return row;
|
|
94
104
|
prisma_1.prisma.apiKey
|
|
95
105
|
.update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
|
|
96
106
|
.catch(() => undefined);
|
|
@@ -99,7 +109,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
99
109
|
}
|
|
100
110
|
revoke(id) {
|
|
101
111
|
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
-
yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
|
|
112
|
+
const row = yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
|
|
113
|
+
yield (0, identityChanges_1.identityChanged)(row.userId);
|
|
103
114
|
});
|
|
104
115
|
}
|
|
105
116
|
list() {
|
|
@@ -240,7 +240,11 @@ let InterceptorService = class InterceptorService {
|
|
|
240
240
|
throw new Error(`Interceptor not active for session ${sessionId}`);
|
|
241
241
|
return state;
|
|
242
242
|
}
|
|
243
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* `udid` is the session's phone. The dashboard events reach only admins who
|
|
245
|
+
* see it: a captured request's headers and bodies can carry the app's
|
|
246
|
+
* sign-in tokens, and REST's /interceptor routes are admin-only too.
|
|
247
|
+
*/
|
|
244
248
|
emit(evt, udid) {
|
|
245
249
|
for (const l of this.listeners) {
|
|
246
250
|
try {
|
|
@@ -255,7 +259,7 @@ let InterceptorService = class InterceptorService {
|
|
|
255
259
|
broadcast(evt, udid) {
|
|
256
260
|
try {
|
|
257
261
|
const socket = typedi_1.Container.get(SocketServer_1.SocketServer);
|
|
258
|
-
const scope = { udid };
|
|
262
|
+
const scope = { udid, adminOnly: true };
|
|
259
263
|
if (evt.type === 'request') {
|
|
260
264
|
void socket.emitToDashboardForDevices(SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, evt.payload, scope);
|
|
261
265
|
}
|
|
@@ -26,6 +26,7 @@ const event_manager_1 = require("../dashboard/event-manager");
|
|
|
26
26
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
27
27
|
const device_service_1 = require("../data-service/device-service");
|
|
28
28
|
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
29
|
+
const TracingService_1 = require("./TracingService");
|
|
29
30
|
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
30
31
|
let OrphanSweeper = class OrphanSweeper {
|
|
31
32
|
constructor() {
|
|
@@ -76,6 +77,11 @@ let OrphanSweeper = class OrphanSweeper {
|
|
|
76
77
|
// the ledger's, at boot (PhoneNetworkRestore).
|
|
77
78
|
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
|
|
78
79
|
(0, sessionMemory_1.forgetSessionMemory)(s.id);
|
|
80
|
+
// A previous process's session has no span here; this one's ends.
|
|
81
|
+
typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(s.id, {
|
|
82
|
+
failed: true,
|
|
83
|
+
reason: 'Session heartbeat timeout',
|
|
84
|
+
});
|
|
79
85
|
// This session's claim only, not every row with its udid: the phone
|
|
80
86
|
// may be another session's by now (deviceClaims.ts).
|
|
81
87
|
yield (0, device_service_1.releaseSessionDevices)(s.id);
|
|
@@ -51,6 +51,10 @@ function whereTuple(strategy, selector) {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
/** Just the selector a change is about, for the socket server's scoped emit. */
|
|
55
|
+
function selectorOf(tuple) {
|
|
56
|
+
return { strategy: tuple.strategy, selector: tuple.selector };
|
|
57
|
+
}
|
|
54
58
|
/** The SelectorEvent row for one change. */
|
|
55
59
|
function selectorEventData(tuple, action, userId, reason) {
|
|
56
60
|
return {
|
|
@@ -140,7 +144,7 @@ let SelectorStateService = class SelectorStateService {
|
|
|
140
144
|
return upserted;
|
|
141
145
|
}));
|
|
142
146
|
scopedLog.info(`markFixed: ${ctx.strategy}=${ctx.selector} → pending (api_key=${ctx.apiKeyId})`);
|
|
143
|
-
this.socket.
|
|
147
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row), selectorOf(ctx));
|
|
144
148
|
return row;
|
|
145
149
|
});
|
|
146
150
|
}
|
|
@@ -174,7 +178,7 @@ let SelectorStateService = class SelectorStateService {
|
|
|
174
178
|
return upserted;
|
|
175
179
|
}));
|
|
176
180
|
scopedLog.info(`mute: ${ctx.strategy}=${ctx.selector} → muted (api_key=${ctx.apiKeyId})`);
|
|
177
|
-
this.socket.
|
|
181
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row), selectorOf(ctx));
|
|
178
182
|
return row;
|
|
179
183
|
});
|
|
180
184
|
}
|
|
@@ -231,13 +235,13 @@ let SelectorStateService = class SelectorStateService {
|
|
|
231
235
|
scopedLog.info(`unmute: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
|
|
232
236
|
// Emit the post-state. When the row was deleted, surface the prior tuple
|
|
233
237
|
// so the dashboard can drop it from the muted list.
|
|
234
|
-
this.socket.
|
|
238
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, result
|
|
235
239
|
? serialize(result)
|
|
236
240
|
: {
|
|
237
241
|
original_strategy: ctx.strategy,
|
|
238
242
|
original_selector: ctx.selector,
|
|
239
243
|
status: 'deleted',
|
|
240
|
-
});
|
|
244
|
+
}, selectorOf(ctx));
|
|
241
245
|
return result;
|
|
242
246
|
});
|
|
243
247
|
}
|
|
@@ -279,13 +283,13 @@ let SelectorStateService = class SelectorStateService {
|
|
|
279
283
|
return row;
|
|
280
284
|
}));
|
|
281
285
|
scopedLog.info(`cancelVerification: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
|
|
282
|
-
this.socket.
|
|
286
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
|
|
283
287
|
? serialize(result)
|
|
284
288
|
: {
|
|
285
289
|
original_strategy: ctx.strategy,
|
|
286
290
|
original_selector: ctx.selector,
|
|
287
291
|
status: 'deleted',
|
|
288
|
-
});
|
|
292
|
+
}, selectorOf(ctx));
|
|
289
293
|
return result;
|
|
290
294
|
});
|
|
291
295
|
}
|
|
@@ -329,7 +333,7 @@ let SelectorStateService = class SelectorStateService {
|
|
|
329
333
|
return updated;
|
|
330
334
|
}));
|
|
331
335
|
scopedLog.warn(`onHealRecorded: ${ctx.strategy}=${ctx.selector} regressed from ${existing.status} (session=${ctx.sessionId})`);
|
|
332
|
-
this.socket.
|
|
336
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row), selectorOf(ctx));
|
|
333
337
|
return row;
|
|
334
338
|
});
|
|
335
339
|
}
|