@xenon-device-management/xenon 1.29.0 → 2.0.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.
Files changed (190) hide show
  1. package/README.md +11 -2
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-Ne9IA8t9.js → AnnotationOverlay-BmTPCAds.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-CwXbDjtR.js → ApiKeyGate-B5eB7f8B.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-D8694Ndn.js → BugReportButton-DKYsOdGV.js} +1 -1
  6. package/lib/public/assets/{DeviceMosaicView-vqKa_a7b.js → DeviceMosaicView-9DUoZnQi.js} +1 -1
  7. package/lib/public/assets/{EmptyState-DqbEejWx.js → EmptyState-lvyNiXev.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BNKuUsHt.js → FieldGroup-BduMPM9K.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-CLqoSOsz.js → FilterMenu-BSqX6I94.js} +1 -1
  10. package/lib/public/assets/{Menu-CU57vgsd.js → Menu-Ba5cT7jZ.js} +1 -1
  11. package/lib/public/assets/{Modal-Dt2whV22.js → Modal-CceoMY7D.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-BipS3IoG.js → RecordingPage-6T9dOidc.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-DORtXeH5.js → RecordingsPage-qj6FdZXR.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-IX6aSFFO.js → SegmentedControl-CSRuZ8TR.js} +1 -1
  15. package/lib/public/assets/{SettingCard-DDp2N5zA.js → SettingCard-Bp1kz4By.js} +1 -1
  16. package/lib/public/assets/{Table-CiaA4Dni.js → Table-LV7zFyZg.js} +1 -1
  17. package/lib/public/assets/{activity-DzhwJvJO.js → activity-D17TUloi.js} +1 -1
  18. package/lib/public/assets/{ai-settings-BllB4M52.js → ai-settings-qvbhY89P.js} +1 -1
  19. package/lib/public/assets/api-keys-BfpvdG7G.js +1 -0
  20. package/lib/public/assets/apps-DMhpZuKd.css +1 -0
  21. package/lib/public/assets/apps-Dbnzd0fW.js +26 -0
  22. package/lib/public/assets/{arrow-left-poHDsOnw.js → arrow-left-DD1VjtC7.js} +1 -1
  23. package/lib/public/assets/{arrow-right-Bt8ka8uH.js → arrow-right-D0pnM3ma.js} +1 -1
  24. package/lib/public/assets/{arrow-up-right-C7zjEfIX.js → arrow-up-right-BIC8JOV6.js} +1 -1
  25. package/lib/public/assets/{auth-shell-DqNP66nv.js → auth-shell-DWHPXSnI.js} +2 -2
  26. package/lib/public/assets/{builds-page-BmoSSMe1.js → builds-page-ClCNBUv_.js} +1 -1
  27. package/lib/public/assets/{button-DujkWRKD.js → button-xtAhYR-f.js} +1 -1
  28. package/lib/public/assets/{calendar-3xj_SfAt.js → calendar-CupwJ7Bm.js} +1 -1
  29. package/lib/public/assets/{check-DAtcemi7.js → check-CyuEoKkd.js} +1 -1
  30. package/lib/public/assets/{chevron-left-BODVavmZ.js → chevron-left-DgX-F9xK.js} +1 -1
  31. package/lib/public/assets/{chevron-right-BtzJa9CV.js → chevron-right-BKJgJnU9.js} +1 -1
  32. package/lib/public/assets/{circle-check-CNLy00dn.js → circle-check-CZdg8mMa.js} +1 -1
  33. package/lib/public/assets/{circle-x-DwlxOXJV.js → circle-x-CZH6t9z-.js} +1 -1
  34. package/lib/public/assets/{clock-CthJSgmT.js → clock-Cd-t7xpe.js} +1 -1
  35. package/lib/public/assets/{copy-o_vwnvDl.js → copy-C6zerX3I.js} +1 -1
  36. package/lib/public/assets/{device-explorer-BGwZQSit.js → device-explorer-B1pI6b_M.js} +2 -2
  37. package/lib/public/assets/device-explorer-DV8rWRxL.css +1 -0
  38. package/lib/public/assets/{download-CH7KYHQ6.js → download-x9WP7mcL.js} +1 -1
  39. package/lib/public/assets/{forgot-password-Fo0NoMuQ.js → forgot-password-B0TEH4N-.js} +1 -1
  40. package/lib/public/assets/{funnel-DLq7lVeF.js → funnel-Cb40MfFN.js} +1 -1
  41. package/lib/public/assets/{index-SVMaJ9rC.js → index-BR1wTIPC.js} +9 -9
  42. package/lib/public/assets/{index-tbOE1tW_.js → index-BvH_ekRy.js} +1 -1
  43. package/lib/public/assets/{index-Fa91L6sA.css → index-hymrhlEZ.css} +1 -1
  44. package/lib/public/assets/{input-Cn9aCi1V.js → input-DejcCiTI.js} +1 -1
  45. package/lib/public/assets/{keycodes-DVNcuy4F.js → keycodes-Mavx5iGg.js} +1 -1
  46. package/lib/public/assets/{layers-DFA4vIXT.js → layers-BKqYLiHE.js} +1 -1
  47. package/lib/public/assets/{lock-DbVuQMlE.js → lock-LFsFhc17.js} +1 -1
  48. package/lib/public/assets/{login-kvBEtLEI.js → login-x2pkzt-t.js} +1 -1
  49. package/lib/public/assets/{maintenance-settings-9MSQd7-h.js → maintenance-settings-CYafcoV-.js} +1 -1
  50. package/lib/public/assets/{monitor-C2wiQCzm.js → monitor-_7_uJ8d9.js} +1 -1
  51. package/lib/public/assets/{mouse-pointer-2-BxOt4PTR.js → mouse-pointer-2-BfLuURKN.js} +1 -1
  52. package/lib/public/assets/{network-VOsv-B1C.js → network-Z_zBhpsD.js} +1 -1
  53. package/lib/public/assets/{overview-BxRv07jX.js → overview-BY5uv08S.js} +1 -1
  54. package/lib/public/assets/{page-header-DRYmsZ55.js → page-header-CP2UkykG.js} +1 -1
  55. package/lib/public/assets/{plus-BY8JP9c5.js → plus-BTD0dnQ0.js} +1 -1
  56. package/lib/public/assets/{profile-page-C0IMKJRO.js → profile-page-BhJZuCS7.js} +1 -1
  57. package/lib/public/assets/{reset-password-2OxcQdve.js → reset-password-CMN0SO81.js} +1 -1
  58. package/lib/public/assets/{runbook-page-ChzDYLnk.js → runbook-page-CBpPX9_5.js} +1 -1
  59. package/lib/public/assets/{select-DkLtE2T3.js → select-Uc9E5pye.js} +1 -1
  60. package/lib/public/assets/{selector-detail-page-9XKCgLLk.js → selector-detail-page-BSyBnRuU.js} +1 -1
  61. package/lib/public/assets/{selector-health-CZb7lL82.js → selector-health-DzqxNkzm.js} +1 -1
  62. package/lib/public/assets/{selector-health-page-CgNkXERq.js → selector-health-page-CyPLjoD1.js} +1 -1
  63. package/lib/public/assets/{session-detail-page-V68omLzd.js → session-detail-page-BZQZN1P-.js} +1 -1
  64. package/lib/public/assets/{settings-BTO5_h8Z.js → settings-CkSWiZq8.js} +1 -1
  65. package/lib/public/assets/{status-pill-outline-DteJiQpj.js → status-pill-outline-BFYni2qo.js} +1 -1
  66. package/lib/public/assets/{tablet-BvW2cqBO.js → tablet-BSd-xSZK.js} +1 -1
  67. package/lib/public/assets/teams-PpoEQz31.js +1 -0
  68. package/lib/public/assets/{trash-2-Ck4Hj9AT.js → trash-2-BorJv5_u.js} +1 -1
  69. package/lib/public/assets/{upload-DOZwUk1H.js → upload-C3i7sAZl.js} +1 -1
  70. package/lib/public/assets/{use-password-reset-mode-BkBlX2qd.js → use-password-reset-mode-CeZQ3EsH.js} +1 -1
  71. package/lib/public/assets/{users-yoXht7X_.js → users-CEJ3fC78.js} +1 -1
  72. package/lib/public/assets/{users-CkggsUeX.js → users-Drd7ifJx.js} +1 -1
  73. package/lib/public/assets/{video-off-DZG2o_Gl.js → video-off-CNzn6bPE.js} +1 -1
  74. package/lib/public/assets/{webhook-settings-D31jQ3yC.js → webhook-settings-BFqX6QXV.js} +1 -1
  75. package/lib/public/assets/{zap-nAiJWOGv.js → zap-4yN8Jdfw.js} +1 -1
  76. package/lib/public/index.html +2 -2
  77. package/lib/schema.json +1 -1
  78. package/lib/src/XenonCapabilityManager.js +21 -50
  79. package/lib/src/app/index.js +14 -9
  80. package/lib/src/app/insertBeforeRoutes.js +57 -0
  81. package/lib/src/app/registerCommandAuth.js +67 -0
  82. package/lib/src/app/routers/apps.js +83 -5
  83. package/lib/src/app/routers/auth.js +1 -1
  84. package/lib/src/app/routers/bug-report.js +12 -0
  85. package/lib/src/app/routers/capabilities.js +4 -0
  86. package/lib/src/app/routers/control.js +6 -1
  87. package/lib/src/app/routers/dashboard.js +116 -66
  88. package/lib/src/app/routers/grid.js +38 -57
  89. package/lib/src/app/routers/recordings.js +3 -1
  90. package/lib/src/app/swagger-docs.js +7 -4
  91. package/lib/src/app/swagger.js +3 -0
  92. package/lib/src/dashboard/event-manager.js +18 -21
  93. package/lib/src/dashboard/services/app-service.js +24 -2
  94. package/lib/src/data-service/device-service.js +2 -1
  95. package/lib/src/data-service/queue-service.js +4 -3
  96. package/lib/src/device-utils.js +1 -1
  97. package/lib/src/generated/client/edge.js +5 -4
  98. package/lib/src/generated/client/index-browser.js +2 -1
  99. package/lib/src/generated/client/index.d.ts +413 -7
  100. package/lib/src/generated/client/index.js +5 -4
  101. package/lib/src/generated/client/package.json +1 -1
  102. package/lib/src/generated/client/schema.prisma +7 -0
  103. package/lib/src/generated/client/wasm.js +2 -1
  104. package/lib/src/interceptors/CommandInterceptor.js +5 -5
  105. package/lib/src/middleware/authMiddleware.js +44 -36
  106. package/lib/src/middleware/commandAuth.js +146 -0
  107. package/lib/src/middleware/commandCaller.js +142 -0
  108. package/lib/src/middleware/verifyCredential.js +124 -0
  109. package/lib/src/services/ServerManager.js +7 -0
  110. package/lib/src/services/SessionLifecycleService.js +143 -73
  111. package/lib/src/services/TeamService.js +6 -3
  112. package/lib/src/services/device-access/appVisibility.js +27 -0
  113. package/lib/src/services/device-access/queueVisibility.js +118 -0
  114. package/lib/src/services/device-access/sessionVisibility.js +65 -0
  115. package/lib/src/services/healing/HealingOrchestrator.js +11 -3
  116. package/lib/src/services/lease/LeaseService.js +13 -4
  117. package/lib/src/services/lease/buildCapabilityBag.js +4 -1
  118. package/lib/src/services/lease/leaseSessionCaps.js +13 -69
  119. package/lib/src/services/recording/annotation-render.js +33 -1
  120. package/lib/src/services/recording/recordingSummary.js +6 -5
  121. package/lib/src/services/recording/recordingTiming.js +19 -5
  122. package/lib/src/services/session/appCapability.js +50 -0
  123. package/lib/src/services/session/sessionCredentials.js +105 -0
  124. package/lib/src/services/session/xenonOptions.js +71 -0
  125. package/lib/src/services/sessionTokenGate.js +19 -9
  126. package/lib/src/services/token/AppDownloadTicketService.js +73 -0
  127. package/lib/src/services/token/StreamTicketService.js +7 -17
  128. package/lib/src/services/token/singleUseLedger.js +41 -0
  129. package/lib/src/sessions/XenonSession.js +2 -2
  130. package/lib/test/helpers/loopbackServer.js +48 -0
  131. package/lib/test/integration/sdk-leases-router.spec.js +1 -1
  132. package/lib/test/integration/team-visibility-apps.spec.js +181 -0
  133. package/lib/test/integration/team-visibility-recordings.spec.js +4 -0
  134. package/lib/test/integration/team-visibility-sessions.spec.js +274 -0
  135. package/lib/test/unit/AppDownloadTicketService.spec.js +101 -0
  136. package/lib/test/unit/CapabilitiesRouter.spec.js +10 -0
  137. package/lib/test/unit/SessionTokenGate.spec.js +7 -2
  138. package/lib/test/unit/TeamService.test.js +22 -0
  139. package/lib/test/unit/XenonCapabilityManager.spec.js +11 -1
  140. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +202 -0
  141. package/lib/test/unit/annotation-render.spec.js +90 -0
  142. package/lib/test/unit/apps-team-visibility.spec.js +305 -0
  143. package/lib/test/unit/authMiddleware.appTicket.spec.js +201 -0
  144. package/lib/test/unit/bug-report/route.spec.js +49 -2
  145. package/lib/test/unit/cliargs-admin-only.spec.js +182 -0
  146. package/lib/test/unit/command-auth-appium-server.spec.js +184 -0
  147. package/lib/test/unit/command-auth.spec.js +410 -0
  148. package/lib/test/unit/command-caller.spec.js +254 -0
  149. package/lib/test/unit/event-manager-regression-hook.spec.js +7 -1
  150. package/lib/test/unit/extract-access-key-token-pair.test.js +47 -4
  151. package/lib/test/unit/grid-admin-only.spec.js +55 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +24 -1
  153. package/lib/test/unit/insert-before-routes.spec.js +167 -0
  154. package/lib/test/unit/install-repository-app-team.spec.js +116 -0
  155. package/lib/test/unit/lease/LeaseService.spec.js +3 -1
  156. package/lib/test/unit/lease/allocate-lease-branch.spec.js +2 -2
  157. package/lib/test/unit/lease/buildCapabilityBag.spec.js +3 -2
  158. package/lib/test/unit/lease/lease-override.spec.js +1 -1
  159. package/lib/test/unit/lease/lease-session-ownership.spec.js +4 -4
  160. package/lib/test/unit/lease/lease-team-visibility.spec.js +1 -1
  161. package/lib/test/unit/lease/lease-token-log-filter.spec.js +1 -1
  162. package/lib/test/unit/lease/lease-token-oracle.spec.js +207 -0
  163. package/lib/test/unit/lease/session-lease-proof.spec.js +42 -26
  164. package/lib/test/unit/logcat-ws.spec.js +7 -4
  165. package/lib/test/unit/queue-requester.spec.js +211 -0
  166. package/lib/test/unit/queue-team-scope-routes.spec.js +209 -0
  167. package/lib/test/unit/queue-visibility.spec.js +179 -0
  168. package/lib/test/unit/recording-summary.spec.js +22 -0
  169. package/lib/test/unit/recording-timing.spec.js +27 -0
  170. package/lib/test/unit/recordings-library-routes.spec.js +25 -0
  171. package/lib/test/unit/session-app-resolution.spec.js +331 -0
  172. package/lib/test/unit/session-assets.spec.js +167 -0
  173. package/lib/test/unit/session-attribution-wiring.spec.js +134 -24
  174. package/lib/test/unit/session-credential-strip.spec.js +277 -0
  175. package/lib/test/unit/session-credentials.spec.js +149 -0
  176. package/lib/test/unit/session-identity.spec.js +2 -2
  177. package/lib/test/unit/session-visibility.spec.js +109 -0
  178. package/lib/test/unit/stored-session-capabilities.spec.js +54 -0
  179. package/lib/test/unit/verify-credential.spec.js +138 -0
  180. package/lib/test/unit/xenon-options.spec.js +95 -0
  181. package/lib/tsconfig.tsbuildinfo +1 -1
  182. package/package.json +1 -1
  183. package/prisma/migrations/20260928120000_app_team/migration.sql +26 -0
  184. package/prisma/schema.prisma +7 -0
  185. package/schema.json +1 -1
  186. package/lib/public/assets/api-keys-bjXg97iu.js +0 -1
  187. package/lib/public/assets/apps-CQEjt22I.js +0 -21
  188. package/lib/public/assets/apps-DJMD5JOy.css +0 -1
  189. package/lib/public/assets/device-explorer-DTH5oU54.css +0 -1
  190. package/lib/public/assets/teams-8Qo7HmUv.js +0 -1
@@ -49,7 +49,6 @@ exports.XENON_CAPABILITIES = void 0;
49
49
  exports.androidCapabilities = androidCapabilities;
50
50
  exports.iOSCapabilities = iOSCapabilities;
51
51
  exports.extractTeamCap = extractTeamCap;
52
- exports.extractAccessKeyCap = extractAccessKeyCap;
53
52
  exports.extractAccessKeyTokenPair = extractAccessKeyTokenPair;
54
53
  exports.extractSessionToken = extractSessionToken;
55
54
  exports.getXenonCapabilities = getXenonCapabilities;
@@ -58,6 +57,7 @@ const lodash_1 = __importDefault(require("lodash"));
58
57
  const typedi_1 = require("typedi");
59
58
  const PortAllocator_1 = require("./services/PortAllocator");
60
59
  const logger_1 = __importDefault(require("./logger"));
60
+ const xenonOptions_1 = require("./services/session/xenonOptions");
61
61
  var XENON_CAPABILITIES;
62
62
  (function (XENON_CAPABILITIES) {
63
63
  XENON_CAPABILITIES["BUILD_NAME"] = "build";
@@ -68,7 +68,6 @@ var XENON_CAPABILITIES;
68
68
  XENON_CAPABILITIES["LIVE_VIDEO"] = "live_video";
69
69
  XENON_CAPABILITIES["SCREENSHOT_ON_FAILURE"] = "screenshot_on_failure";
70
70
  XENON_CAPABILITIES["SCREENSHOT_ON_FAIL"] = "screenshotOnFailure";
71
- XENON_CAPABILITIES["XENON_OPTIONS"] = "xenon:options";
72
71
  XENON_CAPABILITIES["SAVE_DEVICE_LOGS"] = "saveDeviceLogs";
73
72
  XENON_CAPABILITIES["SAVE_LOGS"] = "save_device_logs";
74
73
  XENON_CAPABILITIES["SCREENSHOT_ON_EVERY_COMMAND"] = "screenshot_on_every_command";
@@ -221,58 +220,33 @@ function extractTeamCap(caps) {
221
220
  }
222
221
  return undefined;
223
222
  }
224
- function extractAccessKeyCap(caps) {
225
- var _a;
226
- const merged = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
227
- const prefixes = ['xenon:', 'xe:', 'appium:', ''];
228
- const names = ['accessKey', 'access_key'];
229
- for (const prefix of prefixes) {
230
- for (const name of names) {
231
- const v = merged[prefix ? `${prefix}${name}` : name];
232
- if (typeof v === 'string' && v.length > 0)
233
- return v;
234
- }
235
- }
236
- return undefined;
237
- }
238
- // Returns the (accessKey, token) pair from df:options.{accessKey,token} or
239
- // equivalently df:options.{access_key,token}. Returns undefined if either
240
- // piece is missing — callers fall back to extractAccessKeyCap (legacy).
223
+ // Returns the API-key pair from xe:options.{accessKey,token} (or the
224
+ // xenon:options alias; xe:options wins field by field). Returns undefined if
225
+ // either piece is missing. This is the only key-pair credential a session can
226
+ // present; there is no flat `xenon:accessKey`.
241
227
  function extractAccessKeyTokenPair(caps) {
242
- var _a, _b, _c, _d;
243
- const merged = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
244
- const dfOptions = (_c = (_b = merged['df:options']) !== null && _b !== void 0 ? _b : merged['xenon:df:options']) !== null && _c !== void 0 ? _c : merged['appium:df:options'];
245
- if (!dfOptions || typeof dfOptions !== 'object')
246
- return undefined;
247
- const accessKey = (_d = dfOptions.accessKey) !== null && _d !== void 0 ? _d : dfOptions.access_key;
248
- const token = dfOptions.token;
249
- if (typeof accessKey !== 'string' || typeof token !== 'string')
250
- return undefined;
251
- if (!accessKey || !token)
252
- return undefined;
253
- return { accessKey, token };
228
+ const options = (0, xenonOptions_1.xenonOptionsOf)(caps);
229
+ const accessKey = (0, xenonOptions_1.stringOption)(options, 'accessKey');
230
+ const token = (0, xenonOptions_1.stringOption)(options, 'token');
231
+ return accessKey && token ? { accessKey, token } : undefined;
254
232
  }
255
- // Returns the xenon:options.sessionToken JWT (hub-minted, aud 'xenon-session'),
256
- // used by the opt-in session-token gate (spec §3 item 6, risk R9). Returns
257
- // null if absent — callers combine this with the df:options access-key/token
258
- // pair to decide whether createSession is authorized.
233
+ // Returns the xe:options.sessionToken JWT (hub-minted, aud 'xenon-session'),
234
+ // or the xenon:options alias's, used by the opt-in session-token gate (spec
235
+ // §3 item 6, risk R9). Returns null if absent — callers combine this with the
236
+ // access-key/token pair to decide whether createSession is authorized.
259
237
  function extractSessionToken(caps) {
260
238
  var _a;
261
- const read = (obj) => {
262
- const xo = obj === null || obj === void 0 ? void 0 : obj['xenon:options'];
263
- const t = xo && typeof xo === 'object' ? xo.sessionToken : undefined;
264
- return typeof t === 'string' && t.length > 0 ? t : null;
265
- };
266
- const fm = Array.isArray(caps === null || caps === void 0 ? void 0 : caps.firstMatch) ? caps.firstMatch[0] : undefined;
267
- return (_a = read(caps === null || caps === void 0 ? void 0 : caps.alwaysMatch)) !== null && _a !== void 0 ? _a : read(fm);
239
+ return (_a = (0, xenonOptions_1.stringOption)((0, xenonOptions_1.xenonOptionsOf)(caps), 'sessionToken')) !== null && _a !== void 0 ? _a : null;
268
240
  }
269
241
  function getXenonCapabilities(caps) {
270
242
  var _a, _b;
271
243
  const mergedCapabilites = Object.assign({}, (_b = (_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : {}, caps.alwaysMatch);
244
+ const xenonOptions = (0, xenonOptions_1.xenonOptionsIn)(mergedCapabilites);
272
245
  const getAnyCap = (snake, camel) => {
273
246
  // Strict prefix resolution: xe:, appium:, no-prefix — snake_case + camelCase fallbacks.
274
- // Also accept the namespaced `xenon:options.<name>` form, which is how other Appium
275
- // plugins document nested options and what most users naturally try first.
247
+ // Also accept the namespaced `xe:options.<name>` form (or its `xenon:options` alias),
248
+ // which is how other Appium plugins document nested options and what most users
249
+ // naturally try first.
276
250
  const prefixes = ['xe:', 'appium:', ''];
277
251
  const names = [snake, camel];
278
252
  for (const prefix of prefixes) {
@@ -282,12 +256,9 @@ function getXenonCapabilities(caps) {
282
256
  return mergedCapabilites[key];
283
257
  }
284
258
  }
285
- const xenonOptions = mergedCapabilites[XENON_CAPABILITIES.XENON_OPTIONS];
286
- if (xenonOptions && typeof xenonOptions === 'object') {
287
- for (const name of names) {
288
- if (xenonOptions[name] !== undefined)
289
- return xenonOptions[name];
290
- }
259
+ for (const name of names) {
260
+ if (xenonOptions[name] !== undefined)
261
+ return xenonOptions[name];
291
262
  }
292
263
  return undefined;
293
264
  };
@@ -55,8 +55,7 @@ const cors_1 = __importDefault(require("cors"));
55
55
  const async_lock_1 = __importDefault(require("async-lock"));
56
56
  const crypto_1 = __importDefault(require("crypto"));
57
57
  const InternalHttpClient_1 = require("../InternalHttpClient");
58
- const config_1 = require("../config");
59
- const logger_1 = __importDefault(require("../logger"));
58
+ const logger_1 = __importStar(require("../logger"));
60
59
  const sessionContext_1 = require("../logging/sessionContext");
61
60
  const dashboard_1 = __importDefault(require("./routers/dashboard"));
62
61
  const grid_1 = __importDefault(require("./routers/grid"));
@@ -67,7 +66,7 @@ const reservation_1 = __importDefault(require("./routers/reservation"));
67
66
  const ports_1 = __importDefault(require("./routers/ports"));
68
67
  const sdk_leases_1 = __importDefault(require("./routers/sdk-leases"));
69
68
  const sdk_version_1 = __importDefault(require("./routers/sdk-version"));
70
- const config_2 = __importDefault(require("./routers/config"));
69
+ const config_1 = __importDefault(require("./routers/config"));
71
70
  const interceptor_1 = __importDefault(require("./routers/interceptor"));
72
71
  const bug_report_1 = __importDefault(require("./routers/bug-report"));
73
72
  const recordings_1 = __importDefault(require("./routers/recordings"));
@@ -81,6 +80,8 @@ const capabilities_1 = require("./routers/capabilities");
81
80
  const projects_1 = require("./routers/projects");
82
81
  const audit_1 = require("./routers/audit");
83
82
  const authMiddleware_1 = require("../middleware/authMiddleware");
83
+ const roleGuard_1 = require("../middleware/roleGuard");
84
+ const scopeGuard_1 = require("../middleware/scopeGuard");
84
85
  const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware");
85
86
  const csrfMiddleware_1 = require("../middleware/csrfMiddleware");
86
87
  const swagger_1 = require("./swagger");
@@ -214,8 +215,10 @@ router.use((req, res, next) => {
214
215
  });
215
216
  staticFilesRouter.use(express_1.default.static(publicPath, { index: false }));
216
217
  router.use('/api', apiRouter);
217
- // Principal Fix: Rename collision route from /assets to /session-recordings to avoid conflict with dashboard's /assets folder
218
- router.use('/session-recordings', express_1.default.static(config_1.config.sessionAssetsPath));
218
+ // Session files are served by GET /api/session/:sessionId/asset/:kind/:file,
219
+ // behind the login and the session's team check. They used to be an
220
+ // express.static mount here, outside the login, which also served every
221
+ // recording and interceptor capture stored under the same folder.
219
222
  router.use(staticFilesRouter);
220
223
  function createRouter(pluginArgs) {
221
224
  // CSRF defense (runs before /health so even a hostile GET->POST confused-
@@ -253,9 +256,11 @@ function createRouter(pluginArgs) {
253
256
  // MCP audit ingest: the 2b gateway ships batched tool-call audit JSONL here,
254
257
  // authenticated as a service identity (roleGuard(MEMBER) + admin scope).
255
258
  apiRouter.use('/audit', (0, audit_1.auditRouter)());
256
- // Exposes plugin CLI args (may include host, hub URL, etc.) — auth-gated.
257
- apiRouter.get('/cliArgs', (_req, res) => __awaiter(this, void 0, void 0, function* () {
258
- res.json(yield (0, pluginArgs_1.getCLIArgs)());
259
+ // The stored Appium arguments: a database URL with its password, AI and
260
+ // cloud provider keys, the hub URL. Admin-only, like /processes, and
261
+ // redacted the way the startup log redacts them.
262
+ apiRouter.get('/cliArgs', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), (_req, res) => __awaiter(this, void 0, void 0, function* () {
263
+ res.json((0, logger_1.redactSecrets)(yield (0, pluginArgs_1.getCLIArgs)()));
259
264
  }));
260
265
  // Prometheus-style metrics — auth-gated to avoid operational recon.
261
266
  apiRouter.get('/metrics', (_req, res) => __awaiter(this, void 0, void 0, function* () {
@@ -269,7 +274,7 @@ function createRouter(pluginArgs) {
269
274
  control_1.default.register(apiRouter);
270
275
  apps_1.default.register(apiRouter);
271
276
  webhook_1.default.register(apiRouter);
272
- config_2.default.register(apiRouter, pluginArgs);
277
+ config_1.default.register(apiRouter, pluginArgs);
273
278
  interceptor_1.default.register(apiRouter);
274
279
  bug_report_1.default.register(apiRouter);
275
280
  recordings_1.default.register(apiRouter);
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routerStackOf = routerStackOf;
4
+ exports.insertBeforeRoutes = insertBeforeRoutes;
5
+ function isRouter(candidate) {
6
+ return !!candidate && Array.isArray(candidate.stack);
7
+ }
8
+ function findRouter(app) {
9
+ if (!app)
10
+ return undefined;
11
+ if (isRouter(app._router))
12
+ return { router: app._router, shape: 'express4' };
13
+ let router;
14
+ try {
15
+ // Express 4 defines `app.router` as a getter that throws a deprecation
16
+ // error. Express 5 defines it as the router itself.
17
+ router = app.router;
18
+ }
19
+ catch (_a) {
20
+ router = undefined;
21
+ }
22
+ if (isRouter(router))
23
+ return { router, shape: 'express5' };
24
+ return undefined;
25
+ }
26
+ /** The app's router stack, or undefined when neither Express shape has one. */
27
+ function routerStackOf(app) {
28
+ var _a;
29
+ return (_a = findRouter(app)) === null || _a === void 0 ? void 0 : _a.router.stack;
30
+ }
31
+ function insertBeforeRoutes(app, path, handler) {
32
+ const found = findRouter(app);
33
+ if (!found) {
34
+ return {
35
+ placed: false,
36
+ reason: 'no Express router found (neither app._router nor app.router has a stack)',
37
+ };
38
+ }
39
+ const { stack } = found.router;
40
+ const lengthBefore = stack.length;
41
+ app.use(path, handler);
42
+ const added = stack.splice(lengthBefore);
43
+ if (added.length !== 1) {
44
+ // app.use() added its layer somewhere other than the stack we found, or
45
+ // added more than one. Put back whatever it added and refuse to guess.
46
+ stack.push(...added);
47
+ return {
48
+ placed: false,
49
+ shape: found.shape,
50
+ reason: `app.use() added ${added.length} layers to the router stack, expected 1`,
51
+ };
52
+ }
53
+ const firstRoute = stack.findIndex((layer) => !!layer.route);
54
+ const index = firstRoute === -1 ? stack.length : firstRoute;
55
+ stack.splice(index, 0, added[0]);
56
+ return { placed: true, shape: found.shape, index };
57
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.normalizeBasePath = normalizeBasePath;
7
+ exports.sessionCommandPath = sessionCommandPath;
8
+ exports.registerCommandAuth = registerCommandAuth;
9
+ const typedi_1 = require("typedi");
10
+ const logger_1 = __importDefault(require("../logger"));
11
+ const config_1 = require("../config");
12
+ const SessionOwnerResolver_1 = require("../services/device-access/SessionOwnerResolver");
13
+ const commandAuth_1 = require("../middleware/commandAuth");
14
+ const commandCaller_1 = require("../middleware/commandCaller");
15
+ const insertBeforeRoutes_1 = require("./insertBeforeRoutes");
16
+ /**
17
+ * Appium's base-path rule (base-driver `normalizeBasePath`): drop one trailing
18
+ * '/', and add a leading '/' unless the path is empty. `cliArgs.basePath`
19
+ * reaches a plugin as the operator wrote it, while Appium normalises its own
20
+ * copy before building routes, so the guard must normalise the same way or it
21
+ * would miss the routes it is meant to sit in front of.
22
+ */
23
+ function normalizeBasePath(basePath) {
24
+ let normalized = typeof basePath === 'string' ? basePath : '';
25
+ normalized = normalized.replace(/\/$/, '');
26
+ if (normalized !== '' && !normalized.startsWith('/'))
27
+ normalized = `/${normalized}`;
28
+ return normalized;
29
+ }
30
+ /**
31
+ * Everything under `<basePath>/session/:sessionId`, every method. The create
32
+ * route (`POST <basePath>/session`) has no id segment and does not match.
33
+ */
34
+ function sessionCommandPath(basePath) {
35
+ return `${normalizeBasePath(basePath)}/session/:sessionId`;
36
+ }
37
+ /**
38
+ * Install per-command auth (commandAuth.ts) ahead of Appium's routes. Called
39
+ * from ServerManager.registerRoutes, which Appium runs after adding them.
40
+ *
41
+ * The middleware is always installed and reads the setting per request, so a
42
+ * server with the setting off pays one env check per session command and does
43
+ * no lookups. If it cannot be placed ahead of the routes the error is logged,
44
+ * and with the setting on (and auth enabled) the server refuses to start:
45
+ * an operator who asked for the check must not get a server that silently
46
+ * serves session commands without it.
47
+ */
48
+ function registerCommandAuth(app, cliArgs, overrides = {}) {
49
+ const deps = Object.assign({ enabled: () => (0, commandAuth_1.commandAuthEnabled)(), authDisabled: () => config_1.config.authDisabled === true, verifier: new commandCaller_1.CommandCallerVerifier(), ownerOf: (sessionId) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownerOf(sessionId), logger: logger_1.default.scope('CommandAuth') }, overrides);
50
+ const path = sessionCommandPath(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
51
+ const result = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, path, (0, commandAuth_1.createCommandAuthMiddleware)(deps));
52
+ const active = deps.enabled() && !deps.authDisabled();
53
+ if (!result.placed) {
54
+ deps.logger.error(`Per-command auth could not be placed ahead of Appium's routes for ${path}: ${result.reason}.`);
55
+ if (active) {
56
+ throw new Error('XENON_REQUIRE_COMMAND_AUTH is on, but the per-command check could not be placed ahead ' +
57
+ "of Appium's WebDriver routes, so it would never run. Refusing to start rather than " +
58
+ 'serve session commands unchecked.');
59
+ }
60
+ return result;
61
+ }
62
+ deps.logger.info(active
63
+ ? `Per-command auth is ON: every request under ${path} needs the session owner's ` +
64
+ 'credentials or an override admin (XENON_REQUIRE_COMMAND_AUTH).'
65
+ : `Per-command auth is off for ${path} (XENON_REQUIRE_COMMAND_AUTH not set, or auth disabled).`);
66
+ return result;
67
+ }
@@ -20,15 +20,22 @@ const logger_1 = __importDefault(require("../../logger"));
20
20
  const fs_extra_1 = __importDefault(require("fs-extra"));
21
21
  const scopeGuard_1 = require("../../middleware/scopeGuard");
22
22
  const roleGuard_1 = require("../../middleware/roleGuard");
23
+ const appVisibility_1 = require("../../services/device-access/appVisibility");
24
+ const prisma_1 = require("../../prisma");
23
25
  const router = (0, express_1.Router)();
24
26
  // MEMBER-tier baseline: device app visibility requires authenticated user
25
27
  router.use((0, roleGuard_1.roleGuard)('MEMBER'));
26
28
  // Uploading an APK/IPA or deleting one from the fleet requires devices scope.
27
29
  // App listings stay readable to any authenticated key.
28
30
  router.use((0, scopeGuard_1.mutationScopeGuard)(['devices']));
31
+ // Uploaded apps follow the device team rule on the app's team (canSeeApp):
32
+ // admins see every app, members shared apps and their teams'. An app the
33
+ // caller can't see answers every route exactly as an unknown id.
34
+ const UNKNOWN_APP = { error: 'App not found' };
29
35
  router.get('/', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
36
+ var _a;
30
37
  try {
31
- const apps = yield app_service_1.APP_SERVICE.getApps();
38
+ const apps = yield app_service_1.APP_SERVICE.getApps((_a = req.auth) === null || _a === void 0 ? void 0 : _a.teamIds);
32
39
  res.json(apps);
33
40
  }
34
41
  catch (err) {
@@ -57,15 +64,29 @@ router.get('/', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
57
64
  * is not joined onto a path.
58
65
  */
59
66
  exports.DOWNLOAD_OPTIONS = { dotfiles: 'allow' };
67
+ /**
68
+ * Whether this request may see the app. A download ticket (authMiddleware's
69
+ * app-ticket path) was team-checked when it was minted and is bound to one
70
+ * app, so it sees that app and nothing else; everyone else by the team rule.
71
+ */
72
+ function canRequestSeeApp(req, app) {
73
+ var _a, _b;
74
+ if (!app)
75
+ return false;
76
+ if (((_a = req.auth) === null || _a === void 0 ? void 0 : _a.kind) === 'app-ticket')
77
+ return req.auth.appId === app.id;
78
+ return (0, appVisibility_1.canSeeApp)(app, (_b = req.auth) === null || _b === void 0 ? void 0 : _b.teamIds);
79
+ }
60
80
  function downloadApp(req, res) {
61
81
  return __awaiter(this, void 0, void 0, function* () {
62
82
  try {
63
83
  const app = yield app_service_1.APP_SERVICE.getAppById(req.params.id);
64
- if (app && (yield fs_extra_1.default.exists(app.filepath))) {
84
+ // The ticket path sends with DOWNLOAD_OPTIONS too: it is this same call.
85
+ if (app && canRequestSeeApp(req, app) && (yield fs_extra_1.default.exists(app.filepath))) {
65
86
  res.download(app.filepath, app.filename, exports.DOWNLOAD_OPTIONS);
66
87
  }
67
88
  else {
68
- res.status(404).json({ error: 'App not found' });
89
+ res.status(404).json(UNKNOWN_APP);
69
90
  }
70
91
  }
71
92
  catch (err) {
@@ -75,7 +96,26 @@ function downloadApp(req, res) {
75
96
  });
76
97
  }
77
98
  router.get('/:id/download', downloadApp);
99
+ /**
100
+ * The team an upload or a move names: a string id, or null for the shared
101
+ * pool. Undefined, null and '' all mean shared; anything else that is not a
102
+ * string is refused.
103
+ */
104
+ function parseTeamId(value) {
105
+ if (value === undefined || value === null || value === '')
106
+ return { ok: true, teamId: null };
107
+ if (typeof value === 'string')
108
+ return { ok: true, teamId: value };
109
+ return { ok: false };
110
+ }
111
+ function teamExists(teamId) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ const team = yield prisma_1.prisma.team.findUnique({ where: { id: teamId }, select: { id: true } });
114
+ return !!team;
115
+ });
116
+ }
78
117
  router.post('/upload', (0, roleGuard_1.roleGuard)('ADMIN'), (req, res) => __awaiter(void 0, void 0, void 0, function* () {
118
+ var _a;
79
119
  if (!req.files || Object.keys(req.files).length === 0) {
80
120
  return res.status(400).json({ error: 'No files were uploaded.' });
81
121
  }
@@ -83,9 +123,18 @@ router.post('/upload', (0, roleGuard_1.roleGuard)('ADMIN'), (req, res) => __awai
83
123
  if (!appFile) {
84
124
  return res.status(400).json({ error: 'Field "app" is required.' });
85
125
  }
126
+ // Optional multipart field. Checked before anything is written, so a bad
127
+ // team stores nothing.
128
+ const team = parseTeamId((_a = req.body) === null || _a === void 0 ? void 0 : _a.teamId);
129
+ if (!team.ok)
130
+ return res.status(400).json({ error: 'teamId must be a string' });
86
131
  try {
87
- const app = yield app_service_1.APP_SERVICE.uploadApp(appFile);
88
- logger_1.default.audit('APP_UPLOAD', req.ip, { appId: app.id, name: app.name });
132
+ if (team.teamId && !(yield teamExists(team.teamId))) {
133
+ return res.status(400).json({ error: 'team not found' });
134
+ }
135
+ // An upload of bytes already stored returns that app, in its own team.
136
+ const app = yield app_service_1.APP_SERVICE.uploadApp(appFile, team.teamId);
137
+ logger_1.default.audit('APP_UPLOAD', req.ip, { appId: app.id, name: app.name, teamId: app.teamId });
89
138
  res.json(app);
90
139
  }
91
140
  catch (err) {
@@ -93,8 +142,37 @@ router.post('/upload', (0, roleGuard_1.roleGuard)('ADMIN'), (req, res) => __awai
93
142
  res.status(500).json({ error: err.message });
94
143
  }
95
144
  }));
145
+ /**
146
+ * Moves an app to a team, or back to the shared pool with `{ teamId: null }`.
147
+ * Shaped like PUT /grid/device/:udid/team: admin role and scope, 404 for an
148
+ * unknown team or app, `{ ok, updated }`.
149
+ */
150
+ router.put('/:id/team', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(void 0, void 0, void 0, function* () {
151
+ var _a;
152
+ const team = parseTeamId((_a = req.body) === null || _a === void 0 ? void 0 : _a.teamId);
153
+ if (!team.ok)
154
+ return res.status(400).json({ error: 'teamId must be a string or null' });
155
+ try {
156
+ if (team.teamId && !(yield teamExists(team.teamId))) {
157
+ return res.status(404).json({ error: 'team not found' });
158
+ }
159
+ const updated = yield app_service_1.APP_SERVICE.setAppTeam(req.params.id, team.teamId);
160
+ if (updated === 0)
161
+ return res.status(404).json(UNKNOWN_APP);
162
+ logger_1.default.audit('APP_TEAM', req.ip, { appId: req.params.id, teamId: team.teamId });
163
+ res.json({ ok: true, updated });
164
+ }
165
+ catch (err) {
166
+ logger_1.default.error(`Failed to move app ${req.params.id}: ${err.message}`);
167
+ res.status(500).json({ error: err.message });
168
+ }
169
+ }));
96
170
  router.delete('/:id', (0, roleGuard_1.roleGuard)('ADMIN'), (req, res) => __awaiter(void 0, void 0, void 0, function* () {
97
171
  try {
172
+ // Unknown and hidden alike: 404 with the unknown body, nothing deleted.
173
+ const app = yield app_service_1.APP_SERVICE.getAppById(req.params.id);
174
+ if (!canRequestSeeApp(req, app))
175
+ return res.status(404).json(UNKNOWN_APP);
98
176
  yield app_service_1.APP_SERVICE.deleteApp(req.params.id);
99
177
  logger_1.default.audit('APP_DELETE', req.ip, { appId: req.params.id });
100
178
  res.sendStatus(204);
@@ -64,7 +64,7 @@ function issueToken(auth, body) {
64
64
  roles: grant.roles,
65
65
  }, { audience, ttlSeconds: expiresIn });
66
66
  // Session-token capability (spec §3 item 6 / R9): a sibling credential the
67
- // client injects as `xenon:options.sessionToken` so the Appium createSession
67
+ // client injects as `xe:options.sessionToken` so the Appium createSession
68
68
  // interceptor can refuse tokenless direct-connect sessions when the gate is on.
69
69
  const sessionToken = yield svc.sign({ sub: auth.userId, teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null }, { audience: 'xenon-session', ttlSeconds: expiresIn });
70
70
  return { token, expiresIn, audience, scopes: grant.granular, sessionToken };
@@ -21,6 +21,8 @@ const roleGuard_1 = require("../../middleware/roleGuard");
21
21
  const logger_1 = __importDefault(require("../../logger"));
22
22
  const SocketServer_1 = require("../../services/SocketServer");
23
23
  const SocketEvents_1 = require("../../enums/SocketEvents");
24
+ const prisma_1 = require("../../prisma");
25
+ const sessionVisibility_1 = require("../../services/device-access/sessionVisibility");
24
26
  const router = (0, express_1.Router)();
25
27
  router.use((0, roleGuard_1.roleGuard)('MEMBER'));
26
28
  function parseMode(raw) {
@@ -54,6 +56,16 @@ router.post('/sessions/:sessionId/bug-report', (req, res) => __awaiter(void 0, v
54
56
  }
55
57
  windowSec = parsed;
56
58
  }
59
+ // Another team's session gets the service's own unknown-session answer,
60
+ // and nothing is assembled for it.
61
+ const session = yield prisma_1.prisma.session.findUnique({
62
+ where: { id: sessionId },
63
+ select: { device_udid: true, user_id: true },
64
+ });
65
+ const auth = req.auth;
66
+ if (session && !(yield (0, sessionVisibility_1.canSeeSession)(session, auth))) {
67
+ return res.status(404).json({ error: `Session ${sessionId} not found` });
68
+ }
57
69
  const svc = typedi_1.Container.get(BugReportService_1.BugReportService);
58
70
  let bundle;
59
71
  try {
@@ -4,6 +4,7 @@ exports.buildCapabilities = buildCapabilities;
4
4
  exports.capabilitiesRouter = capabilitiesRouter;
5
5
  const express_1 = require("express");
6
6
  const sessionTokenGate_1 = require("../../services/sessionTokenGate");
7
+ const commandAuth_1 = require("../../middleware/commandAuth");
7
8
  // eslint-disable-next-line @typescript-eslint/no-var-requires
8
9
  const pkg = require('../../../package.json');
9
10
  function buildCapabilities() {
@@ -18,6 +19,9 @@ function buildCapabilities() {
18
19
  projects: true, // Task 8
19
20
  mcpScopedTokens: true, // Tasks 1-3: granular-claim minting available
20
21
  sessionTokenGate: (0, sessionTokenGate_1.sessionTokenGateEnabled)(), // Task 4: live XENON_REQUIRE_SESSION_TOKEN value
22
+ // Live XENON_REQUIRE_COMMAND_AUTH value: when true, every WebDriver
23
+ // session command must carry the x-xenon-* pair or a Bearer token.
24
+ commandAuth: (0, commandAuth_1.commandAuthEnabled)(),
21
25
  },
22
26
  };
23
27
  }
@@ -66,6 +66,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
66
66
  const OmniVisionService_1 = require("../../services/omni-vision/OmniVisionService");
67
67
  const InspectorService_1 = require("../../services/InspectorService");
68
68
  const StreamTicketService_1 = require("../../services/token/StreamTicketService");
69
+ const appVisibility_1 = require("../../services/device-access/appVisibility");
69
70
  const PluginContext_1 = require("../../PluginContext");
70
71
  const streamType_1 = require("./streamType");
71
72
  const iosStreamPort_1 = require("./iosStreamPort");
@@ -496,6 +497,7 @@ router.post('/:udid/install', (req, res) => __awaiter(void 0, void 0, void 0, fu
496
497
  res.status(400).send('Manager not found or installApp not supported');
497
498
  }));
498
499
  router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
500
+ var _a;
499
501
  const { udid } = req.params;
500
502
  const { appId } = req.body;
501
503
  const device = yield getDeviceInfo(udid);
@@ -504,8 +506,11 @@ router.post('/:udid/install-repository-app', (req, res) => __awaiter(void 0, voi
504
506
  try {
505
507
  const { APP_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../dashboard/services/app-service')));
506
508
  const app = yield APP_SERVICE.getAppById(appId);
507
- if (!app)
509
+ // Installing hands over the binary as a download does: another team's
510
+ // app answers as an unknown one.
511
+ if (!(0, appVisibility_1.canSeeApp)(app, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.teamIds)) {
508
512
  return res.status(404).send('App not found in repository');
513
+ }
509
514
  const manager = yield getDeviceManagerForPlatform(device.platform);
510
515
  if (manager && manager.installApp) {
511
516
  // If it's a local file, pass the path.