| 1 | import { |
| 2 | CLIENT_GUARDED_RPC_SCOPES, |
| 3 | type DeviceListInput, |
| 4 | clientRpcRequiredScopes, |
| 5 | authScopeRequiredResponse, |
| 6 | AssetCreateUrlInput, |
| 7 | AuthAccessReadScope, |
| 8 | ServerSettingsPatch, |
| 9 | ProviderInstanceMutation, |
| 10 | requiredScopesForServerSettingsPatch, |
| 11 | requiredScopesForProjectMutation, |
| 12 | ProjectMutation, |
| 13 | AuthSettingsWriteScope, |
| 14 | AuthProvidersManageScope, |
| 15 | AuthEnvironmentMaintainScope, |
| 16 | AuthFilesystemReadScope, |
| 17 | AuthFilesystemWriteScope, |
| 18 | AuthDiagnosticsReadScope, |
| 19 | AuthOrchestrationOperateScope, |
| 20 | AuthOrchestrationReadScope, |
| 21 | AuthPreviewOperateScope, |
| 22 | AuthRelayReadScope, |
| 23 | AuthRelayWriteScope, |
| 24 | AuthTerminalOperateScope, |
| 25 | ORCHESTRATION_V2_WS_METHODS, |
| 26 | AuthTerminalReadScope, |
| 27 | type AuthEnvironmentScope, |
| 28 | EnvironmentAuthorizationError, |
| 29 | RpcScopeAuthorization, |
| 30 | WS_METHODS, |
| 31 | WsRpcGroup, |
| 32 | } from "@t3tools/contracts"; |
| 33 | import * as Effect from "effect/Effect"; |
| 34 | import * as Schema from "effect/Schema"; |
| 35 | import * as Layer from "effect/Layer"; |
| 36 | import type * as RpcGroup from "effect/rpc/RpcGroup"; |
| 37 | |
| 38 | type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"]; |
| 39 | |
| 40 | /** |
| 41 | * Keep authorization coverage coupled to the RPC group itself. Adding an RPC to |
| 42 | * `WsRpcGroup` without choosing a scope is a type error instead of a production |
| 43 | * runtime failure. |
| 44 | */ |
| 45 | export const RPC_REQUIRED_SCOPES = { |
| 46 | ...CLIENT_GUARDED_RPC_SCOPES, |
| 47 | [WS_METHODS.uiSettingsGet]: AuthOrchestrationReadScope, |
| 48 | [WS_METHODS.uiSettingsSubscribe]: AuthOrchestrationReadScope, |
| 49 | [WS_METHODS.managedWorkspacesStatus]: AuthFilesystemReadScope, |
| 50 | [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope, |
| 51 | [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope, |
| 52 | [ORCHESTRATION_V2_WS_METHODS.stopWorkflow]: AuthOrchestrationOperateScope, |
| 53 | [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope, |
| 54 | [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope, |
| 55 | [ORCHESTRATION_V2_WS_METHODS.searchThreads]: AuthOrchestrationReadScope, |
| 56 | [ORCHESTRATION_V2_WS_METHODS.searchThread]: AuthOrchestrationReadScope, |
| 57 | [ORCHESTRATION_V2_WS_METHODS.searchThreadStream]: AuthOrchestrationReadScope, |
| 58 | [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope, |
| 59 | [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: AuthOrchestrationReadScope, |
| 60 | [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: AuthOrchestrationReadScope, |
| 61 | [ORCHESTRATION_V2_WS_METHODS.launchThread]: AuthOrchestrationOperateScope, |
| 62 | [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: AuthOrchestrationReadScope, |
| 63 | [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope, |
| 64 | [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: AuthOrchestrationReadScope, |
| 65 | [WS_METHODS.projectsMutate]: AuthOrchestrationOperateScope, |
| 66 | [WS_METHODS.serverProbe]: AuthOrchestrationReadScope, |
| 67 | [WS_METHODS.serverGetConfig]: AuthOrchestrationReadScope, |
| 68 | [WS_METHODS.serverRefreshProviders]: AuthOrchestrationReadScope, |
| 69 | [WS_METHODS.serverUpdateProvider]: AuthProvidersManageScope, |
| 70 | [WS_METHODS.providerAuthStart]: AuthProvidersManageScope, |
| 71 | [WS_METHODS.providerConsumeResetCredit]: AuthProvidersManageScope, |
| 72 | [WS_METHODS.providerAuthComplete]: AuthProvidersManageScope, |
| 73 | [WS_METHODS.chatGptReconnectProfile]: AuthProvidersManageScope, |
| 74 | [WS_METHODS.chatGptImportProfile]: AuthProvidersManageScope, |
| 75 | [WS_METHODS.chatGptHandoffSubscribe]: AuthProvidersManageScope, |
| 76 | [WS_METHODS.codexAuthCallbackSubscribe]: AuthProvidersManageScope, |
| 77 | [WS_METHODS.providerAuthRespond]: AuthProvidersManageScope, |
| 78 | [WS_METHODS.providerAuthCancel]: AuthProvidersManageScope, |
| 79 | [WS_METHODS.providerAuthLogout]: AuthProvidersManageScope, |
| 80 | [WS_METHODS.providerAuthSubscribe]: AuthProvidersManageScope, |
| 81 | [WS_METHODS.providerInstallStart]: AuthProvidersManageScope, |
| 82 | [WS_METHODS.providerInstallCancel]: AuthProvidersManageScope, |
| 83 | [WS_METHODS.providerInstallSubscribe]: AuthOrchestrationReadScope, |
| 84 | [WS_METHODS.providerInstallRemove]: AuthProvidersManageScope, |
| 85 | [WS_METHODS.serverUpdateServer]: AuthEnvironmentMaintainScope, |
| 86 | [WS_METHODS.serverUpdateServerWithProgress]: AuthEnvironmentMaintainScope, |
| 87 | [WS_METHODS.serverCommitDesktopUpdate]: AuthEnvironmentMaintainScope, |
| 88 | [WS_METHODS.serverUpsertKeybinding]: AuthSettingsWriteScope, |
| 89 | [WS_METHODS.serverRemoveKeybinding]: AuthSettingsWriteScope, |
| 90 | [WS_METHODS.serverGetStorageCleanupReport]: AuthOrchestrationReadScope, |
| 91 | [WS_METHODS.serverGetSettings]: AuthOrchestrationReadScope, |
| 92 | [WS_METHODS.serverUpdateSettings]: AuthSettingsWriteScope, |
| 93 | [WS_METHODS.serverSearchAcpRegistry]: AuthOrchestrationReadScope, |
| 94 | [WS_METHODS.serverPrepareAcpRegistryAgent]: AuthProvidersManageScope, |
| 95 | [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: AuthProvidersManageScope, |
| 96 | [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: AuthProvidersManageScope, |
| 97 | [WS_METHODS.serverListAcpRegistrySessions]: AuthOrchestrationReadScope, |
| 98 | [WS_METHODS.serverImportAcpRegistrySession]: AuthOrchestrationOperateScope, |
| 99 | [WS_METHODS.serverDeleteAcpRegistrySession]: AuthOrchestrationOperateScope, |
| 100 | [WS_METHODS.serverListAcpRegistryProviders]: AuthOrchestrationReadScope, |
| 101 | [WS_METHODS.serverSetAcpRegistryProvider]: AuthProvidersManageScope, |
| 102 | [WS_METHODS.serverDisableAcpRegistryProvider]: AuthProvidersManageScope, |
| 103 | [WS_METHODS.serverLogoutAcpRegistry]: AuthProvidersManageScope, |
| 104 | [WS_METHODS.serverDiscoverSourceControl]: AuthOrchestrationReadScope, |
| 105 | [WS_METHODS.serverGetTraceDiagnostics]: AuthDiagnosticsReadScope, |
| 106 | [WS_METHODS.serverGetProcessDiagnostics]: AuthDiagnosticsReadScope, |
| 107 | // Load-balancing new threads reads host load; that is part of operating |
| 108 | // threads, not of inspecting diagnostics. |
| 109 | [WS_METHODS.serverGetHostResources]: AuthOrchestrationReadScope, |
| 110 | [WS_METHODS.serverGetProcessResourceHistory]: AuthDiagnosticsReadScope, |
| 111 | [WS_METHODS.serverGetResourceTelemetryHistory]: AuthDiagnosticsReadScope, |
| 112 | [WS_METHODS.serverRetryResourceTelemetry]: AuthDiagnosticsReadScope, |
| 113 | [WS_METHODS.serverGetUsageSummary]: AuthDiagnosticsReadScope, |
| 114 | [WS_METHODS.serverRefreshUsageRates]: AuthDiagnosticsReadScope, |
| 115 | [WS_METHODS.serverSignalProcess]: AuthEnvironmentMaintainScope, |
| 116 | [WS_METHODS.serverReportClientActivity]: AuthOrchestrationReadScope, |
| 117 | [WS_METHODS.serverReportHostPowerState]: AuthEnvironmentMaintainScope, |
| 118 | [WS_METHODS.serverGetBackgroundPolicy]: AuthOrchestrationReadScope, |
| 119 | [WS_METHODS.scheduledTasksList]: AuthOrchestrationReadScope, |
| 120 | [WS_METHODS.scheduledTasksSubscribe]: AuthOrchestrationReadScope, |
| 121 | [WS_METHODS.secretsAnswerRequest]: AuthOrchestrationOperateScope, |
| 122 | // Delivery logs hold request bodies, so they need the same scope as the URL. |
| 123 | [WS_METHODS.scheduledTasksListWebhookDeliveries]: AuthOrchestrationOperateScope, |
| 124 | [WS_METHODS.scheduledTasksGetWebhookDelivery]: AuthOrchestrationOperateScope, |
| 125 | [WS_METHODS.cloudGetRelayClientStatus]: AuthRelayReadScope, |
| 126 | [WS_METHODS.cloudInstallRelayClient]: AuthRelayWriteScope, |
| 127 | [WS_METHODS.mergebotQueue]: AuthOrchestrationReadScope, |
| 128 | [WS_METHODS.mergebotSubscribe]: AuthOrchestrationReadScope, |
| 129 | [WS_METHODS.mergebotSubscribePullRequestStatus]: AuthOrchestrationReadScope, |
| 130 | [WS_METHODS.mergebotSubscribeDeployments]: AuthOrchestrationReadScope, |
| 131 | [WS_METHODS.jjSubscribeStack]: AuthFilesystemReadScope, |
| 132 | [WS_METHODS.jjSnapshot]: AuthFilesystemReadScope, |
| 133 | [WS_METHODS.jjDiff]: AuthFilesystemReadScope, |
| 134 | [WS_METHODS.pullRequestsList]: AuthOrchestrationReadScope, |
| 135 | [WS_METHODS.pullRequestsListStats]: AuthOrchestrationReadScope, |
| 136 | [WS_METHODS.pullRequestsSummary]: AuthOrchestrationReadScope, |
| 137 | [WS_METHODS.pullRequestsRouting]: AuthOrchestrationReadScope, |
| 138 | [WS_METHODS.pullRequestsRoutingIdentity]: AuthOrchestrationReadScope, |
| 139 | [WS_METHODS.pullRequestsStack]: AuthOrchestrationReadScope, |
| 140 | [WS_METHODS.pullRequestsLinkedThreads]: AuthOrchestrationReadScope, |
| 141 | [WS_METHODS.pullRequestsDetail]: AuthOrchestrationReadScope, |
| 142 | [WS_METHODS.pullRequestsPreview]: AuthOrchestrationReadScope, |
| 143 | [WS_METHODS.pullRequestsChecks]: AuthOrchestrationReadScope, |
| 144 | [WS_METHODS.pullRequestsActivity]: AuthOrchestrationReadScope, |
| 145 | [WS_METHODS.pullRequestsReviewGuide]: AuthOrchestrationReadScope, |
| 146 | [WS_METHODS.pullRequestsThreadComments]: AuthOrchestrationReadScope, |
| 147 | [WS_METHODS.pullRequestsDiffFileContents]: AuthOrchestrationReadScope, |
| 148 | [WS_METHODS.pullRequestsFilesViewed]: AuthOrchestrationReadScope, |
| 149 | // Read scope like the reads it un-caches: refreshing is part of reading, and a read-only |
| 150 | // client pressing refresh must not be told it may not look again. |
| 151 | [WS_METHODS.pullRequestsInvalidate]: AuthOrchestrationReadScope, |
| 152 | [WS_METHODS.pullRequestsReportState]: AuthOrchestrationReadScope, |
| 153 | [WS_METHODS.pullRequestsSubscribeRefreshes]: AuthOrchestrationReadScope, |
| 154 | // The candidate list is a read like the detail beside it; asking somebody for a review is a |
| 155 | // write like every other one. |
| 156 | [WS_METHODS.pullRequestsReviewerCandidates]: AuthOrchestrationReadScope, |
| 157 | [WS_METHODS.pullRequestsLabelCandidates]: AuthOrchestrationReadScope, |
| 158 | [WS_METHODS.sourceControlLookupRepository]: AuthOrchestrationReadScope, |
| 159 | [WS_METHODS.subscribeProjectClones]: AuthOrchestrationReadScope, |
| 160 | [WS_METHODS.projectsListEntries]: AuthFilesystemReadScope, |
| 161 | [WS_METHODS.projectsReadFile]: AuthFilesystemReadScope, |
| 162 | [WS_METHODS.projectsWatch]: AuthFilesystemReadScope, |
| 163 | [WS_METHODS.projectsSearchContents]: AuthFilesystemReadScope, |
| 164 | [WS_METHODS.projectsSearchEntries]: AuthFilesystemReadScope, |
| 165 | [WS_METHODS.projectsWriteFile]: AuthFilesystemWriteScope, |
| 166 | [WS_METHODS.projectsEnsureScratch]: AuthOrchestrationOperateScope, |
| 167 | [WS_METHODS.projectsCreateNew]: AuthOrchestrationOperateScope, |
| 168 | [WS_METHODS.shellOpenInEditor]: AuthOrchestrationOperateScope, |
| 169 | [WS_METHODS.filesystemBrowse]: AuthFilesystemReadScope, |
| 170 | [WS_METHODS.filesystemGetMetadata]: AuthFilesystemReadScope, |
| 171 | [WS_METHODS.agentSessionsScan]: AuthOrchestrationReadScope, |
| 172 | [WS_METHODS.agentSessionsImport]: AuthOrchestrationOperateScope, |
| 173 | [WS_METHODS.assetsCreateUrl]: AuthOrchestrationReadScope, |
| 174 | [WS_METHODS.assetsPersistChatAttachments]: AuthOrchestrationOperateScope, |
| 175 | [WS_METHODS.attachmentsCreateUploadUrl]: AuthOrchestrationOperateScope, |
| 176 | [WS_METHODS.attachmentsDelete]: AuthOrchestrationOperateScope, |
| 177 | [WS_METHODS.providerUploadFeedback]: AuthOrchestrationOperateScope, |
| 178 | // An app's tool calls can change things on its server, like a user action. |
| 179 | [WS_METHODS.mcpAppsCallTool]: AuthOrchestrationOperateScope, |
| 180 | [WS_METHODS.mcpAppsToolInfo]: AuthOrchestrationReadScope, |
| 181 | [WS_METHODS.mcpAppsReadResource]: AuthOrchestrationReadScope, |
| 182 | [WS_METHODS.mcpAppsUpdateModelContext]: AuthOrchestrationOperateScope, |
| 183 | [WS_METHODS.subscribeVcsStatus]: AuthOrchestrationReadScope, |
| 184 | [WS_METHODS.subscribeWorktreeSetup]: AuthOrchestrationReadScope, |
| 185 | [WS_METHODS.worktreeSetupCancel]: AuthOrchestrationOperateScope, |
| 186 | [WS_METHODS.subscribeResourceTelemetry]: AuthDiagnosticsReadScope, |
| 187 | [WS_METHODS.vcsRefreshStatus]: AuthOrchestrationReadScope, |
| 188 | [WS_METHODS.gitResolvePullRequest]: AuthOrchestrationReadScope, |
| 189 | [WS_METHODS.vcsListRefs]: AuthOrchestrationReadScope, |
| 190 | [WS_METHODS.reviewGetDiffPreview]: AuthFilesystemReadScope, |
| 191 | [WS_METHODS.reviewGetDiffFileContents]: AuthFilesystemReadScope, |
| 192 | [WS_METHODS.terminalOpen]: AuthTerminalOperateScope, |
| 193 | [WS_METHODS.terminalAttach]: AuthTerminalOperateScope, |
| 194 | [WS_METHODS.terminalObserve]: AuthTerminalReadScope, |
| 195 | [WS_METHODS.terminalWrite]: AuthTerminalOperateScope, |
| 196 | [WS_METHODS.terminalResize]: AuthTerminalOperateScope, |
| 197 | [WS_METHODS.terminalClear]: AuthTerminalOperateScope, |
| 198 | [WS_METHODS.terminalRestart]: AuthTerminalOperateScope, |
| 199 | [WS_METHODS.terminalClose]: AuthTerminalOperateScope, |
| 200 | [WS_METHODS.subscribeTerminalEvents]: AuthTerminalReadScope, |
| 201 | [WS_METHODS.subscribeTerminalMetadata]: AuthTerminalReadScope, |
| 202 | [WS_METHODS.subscribeScriptProgress]: AuthOrchestrationReadScope, |
| 203 | [WS_METHODS.previewOpen]: AuthPreviewOperateScope, |
| 204 | [WS_METHODS.previewNavigate]: AuthPreviewOperateScope, |
| 205 | [WS_METHODS.previewResize]: AuthPreviewOperateScope, |
| 206 | [WS_METHODS.previewAdjust]: AuthPreviewOperateScope, |
| 207 | [WS_METHODS.previewRefresh]: AuthPreviewOperateScope, |
| 208 | [WS_METHODS.previewClose]: AuthPreviewOperateScope, |
| 209 | [WS_METHODS.previewList]: AuthOrchestrationReadScope, |
| 210 | [WS_METHODS.previewClearProfile]: AuthPreviewOperateScope, |
| 211 | [WS_METHODS.previewReportProfiles]: AuthPreviewOperateScope, |
| 212 | [WS_METHODS.previewReportStatus]: AuthPreviewOperateScope, |
| 213 | [WS_METHODS.subscribePreviewEvents]: AuthOrchestrationReadScope, |
| 214 | [WS_METHODS.subscribeDiscoveredLocalServers]: AuthOrchestrationReadScope, |
| 215 | [WS_METHODS.deviceConfigure]: AuthSettingsWriteScope, |
| 216 | [WS_METHODS.deviceTestHost]: AuthSettingsWriteScope, |
| 217 | [WS_METHODS.deviceList]: AuthOrchestrationReadScope, |
| 218 | [WS_METHODS.deviceOpen]: AuthOrchestrationOperateScope, |
| 219 | [WS_METHODS.deviceClose]: AuthOrchestrationOperateScope, |
| 220 | [WS_METHODS.deviceShutdown]: AuthOrchestrationOperateScope, |
| 221 | [WS_METHODS.deviceDetail]: AuthOrchestrationReadScope, |
| 222 | [WS_METHODS.deviceAction]: AuthOrchestrationOperateScope, |
| 223 | [WS_METHODS.subscribeDeviceState]: AuthOrchestrationReadScope, |
| 224 | [WS_METHODS.subscribeServerConfig]: AuthOrchestrationReadScope, |
| 225 | [WS_METHODS.subscribeServerLifecycle]: AuthOrchestrationReadScope, |
| 226 | [WS_METHODS.subscribeAuthAccess]: AuthAccessReadScope, |
| 227 | [WS_METHODS.subscribeBackgroundPolicy]: AuthOrchestrationReadScope, |
| 228 | } as const satisfies Readonly<Record<WsRpcMethod, AuthEnvironmentScope>>; |
| 229 | |
| 230 | export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope { |
| 231 | if (!Object.hasOwn(RPC_REQUIRED_SCOPES, method)) { |
| 232 | throw new Error(`RPC method ${method} has no declared authorization scope.`); |
| 233 | } |
| 234 | const requiredScope = RPC_REQUIRED_SCOPES[method as WsRpcMethod]; |
| 235 | if (requiredScope === undefined) { |
| 236 | throw new Error(`RPC method ${method} has no declared authorization scope.`); |
| 237 | } |
| 238 | return requiredScope; |
| 239 | } |
| 240 | |
| 241 | export const rpcAuthorizationError = (requiredScope: AuthEnvironmentScope) => |
| 242 | new EnvironmentAuthorizationError({ |
| 243 | message: `The authenticated token is missing required scope: ${requiredScope}.`, |
| 244 | ...authScopeRequiredResponse(requiredScope), |
| 245 | }); |
| 246 | |
| 247 | const SettingsUpdate = Schema.Struct({ |
| 248 | patch: ServerSettingsPatch, |
| 249 | providerInstanceMutation: Schema.optionalKey(ProviderInstanceMutation), |
| 250 | }); |
| 251 | |
| 252 | const requiredScopesForSettingsUpdate = (payload: unknown) => { |
| 253 | const input = Schema.decodeUnknownSync(SettingsUpdate)(payload); |
| 254 | const scopes = requiredScopesForServerSettingsPatch(input.patch); |
| 255 | if (input.providerInstanceMutation === undefined) return scopes; |
| 256 | // An atomic provider mutation carries an empty patch unless it also changes settings. |
| 257 | return Object.values(input.patch).every((value) => value === undefined) |
| 258 | ? [AuthProvidersManageScope] |
| 259 | : [...new Set([...scopes, AuthProvidersManageScope])]; |
| 260 | }; |
| 261 | |
| 262 | const decodeProjectMutation = Schema.decodeUnknownSync(ProjectMutation); |
| 263 | |
| 264 | const requiredScopesForRpcCall = ( |
| 265 | method: string, |
| 266 | payload: unknown, |
| 267 | ): ReadonlyArray<AuthEnvironmentScope> => { |
| 268 | if (method === WS_METHODS.serverRetryResourceTelemetry) { |
| 269 | return [AuthEnvironmentMaintainScope, AuthDiagnosticsReadScope]; |
| 270 | } |
| 271 | if (method === WS_METHODS.assetsCreateUrl) { |
| 272 | const { resource } = Schema.decodeUnknownSync(AssetCreateUrlInput)(payload); |
| 273 | return [ |
| 274 | resource._tag === "workspace-file" || |
| 275 | resource._tag === "media-file" || |
| 276 | resource._tag === "draft-workspace-file" |
| 277 | ? AuthFilesystemReadScope |
| 278 | : AuthOrchestrationReadScope, |
| 279 | ]; |
| 280 | } |
| 281 | if (method === WS_METHODS.serverUpdateSettings) return requiredScopesForSettingsUpdate(payload); |
| 282 | if (method === WS_METHODS.projectsMutate) { |
| 283 | return requiredScopesForProjectMutation(decodeProjectMutation(payload)); |
| 284 | } |
| 285 | const guarded = clientRpcRequiredScopes(method, payload); |
| 286 | if (guarded.length > 0) return guarded; |
| 287 | return [requiredScopeForRpcMethod(method)]; |
| 288 | }; |
| 289 | |
| 290 | /** Authorizes every RPC on one connection against that connection's session scopes. */ |
| 291 | export const layer = (scopes: ReadonlyArray<AuthEnvironmentScope>) => |
| 292 | Layer.succeed(RpcScopeAuthorization)((effect, { rpc, payload }) => { |
| 293 | const requiredScopes = requiredScopesForRpcCall(rpc._tag, payload); |
| 294 | const requiredScope = requiredScopes.find((scope) => !scopes.includes(scope)); |
| 295 | return requiredScope === undefined ? effect : Effect.fail(rpcAuthorizationError(requiredScope)); |
| 296 | }); |
| 297 | |
| 298 | /** Retrying can install or restart tools even though ordinary listing is readable. */ |
| 299 | export const requiredScopeForDeviceList = (input: DeviceListInput): AuthEnvironmentScope => |
| 300 | input.retryHostId || input.updateTool |
| 301 | ? AuthOrchestrationOperateScope |
| 302 | : AuthOrchestrationReadScope; |