| author | |
| committer | |
| log | a78760e5e7b11ea51e21a1640fec54bd3f210fd8 |
| tree | e1fdc3e2d273f2cf0ba5edebf56b4b87aeb7767d |
| parent | b05adb70b00feac0883b2e364256100cfab4b6ca |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>6 files changed, 94 insertions(+), 56 deletions(-)
apps/server/src/auth/RpcAuthorization.test.ts+40| ... | @@ -7,11 +7,15 @@ import { | ... | @@ -7,11 +7,15 @@ import { |
| 7 | WsRpcGroup, | 7 | WsRpcGroup, |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | import { describe, expect, it } from "@effect/vitest"; | 9 | import { describe, expect, it } from "@effect/vitest"; |
| 10 | import * as Effect from "effect/Effect"; | ||
| 11 | import * as Layer from "effect/Layer"; | ||
| 12 | import * as RpcTest from "effect/unstable/rpc/RpcTest"; | ||
| 10 | 13 | ||
| 11 | import { | 14 | import { |
| 12 | RPC_REQUIRED_SCOPES, | 15 | RPC_REQUIRED_SCOPES, |
| 13 | requiredScopeForRpcMethod, | 16 | requiredScopeForRpcMethod, |
| 14 | requiredScopeForDeviceList, | 17 | requiredScopeForDeviceList, |
| 18 | rpcScopeAuthorizationLayer, | ||
| 15 | } from "./RpcAuthorization.ts"; | 19 | } from "./RpcAuthorization.ts"; |
| 16 | 20 | ||
| 17 | describe("RPC authorization scopes", () => { | 21 | describe("RPC authorization scopes", () => { |
| ... | @@ -116,3 +120,39 @@ it("requires operate permission for tool updates even alongside a read-only chec | ... | @@ -116,3 +120,39 @@ it("requires operate permission for tool updates even alongside a read-only chec |
| 116 | ); | 120 | ); |
| 117 | expect(requiredScopeForDeviceList({ updateTool: "hub" })).toBe(AuthOrchestrationOperateScope); | 121 | expect(requiredScopeForDeviceList({ updateTool: "hub" })).toBe(AuthOrchestrationOperateScope); |
| 118 | }); | 122 | }); |
| 123 | |||
| 124 | describe("RPC scope middleware", () => { | ||
| 125 | const tested = [WS_METHODS.serverProbe, WS_METHODS.serverRetryResourceTelemetry] as const; | ||
| 126 | const group = WsRpcGroup.omit( | ||
| 127 | ...[...WsRpcGroup.requests.keys()].filter( | ||
| 128 | (tag): tag is Exclude<keyof typeof RPC_REQUIRED_SCOPES, (typeof tested)[number]> => | ||
| 129 | !(tested as ReadonlyArray<string>).includes(tag), | ||
| 130 | ), | ||
| 131 | ); | ||
| 132 | |||
| 133 | it.effect("checks each RPC's declared scope before its handler runs", () => | ||
| 134 | Effect.gen(function* () { | ||
| 135 | const handled: Array<string> = []; | ||
| 136 | const client = yield* RpcTest.makeClient(group).pipe( | ||
| 137 | Effect.provide( | ||
| 138 | Layer.mergeAll( | ||
| 139 | group.toLayerHandler(WS_METHODS.serverProbe, () => Effect.succeed({})), | ||
| 140 | group.toLayerHandler(WS_METHODS.serverRetryResourceTelemetry, () => | ||
| 141 | Effect.sync(() => handled.push("retry")).pipe(Effect.andThen(Effect.never)), | ||
| 142 | ), | ||
| 143 | rpcScopeAuthorizationLayer([AuthOrchestrationReadScope]), | ||
| 144 | ), | ||
| 145 | ), | ||
| 146 | ); | ||
| 147 | |||
| 148 | expect(yield* client[WS_METHODS.serverProbe]({})).toEqual({}); | ||
| 149 | expect( | ||
| 150 | yield* client[WS_METHODS.serverRetryResourceTelemetry]({}).pipe(Effect.flip), | ||
| 151 | ).toMatchObject({ | ||
| 152 | _tag: "EnvironmentAuthorizationError", | ||
| 153 | requiredScope: AuthOrchestrationOperateScope, | ||
| 154 | }); | ||
| 155 | expect(handled).toEqual([]); | ||
| 156 | }).pipe(Effect.scoped), | ||
| 157 | ); | ||
| 158 | }); |
apps/server/src/auth/RpcAuthorization.ts+19| ... | @@ -9,9 +9,13 @@ import { | ... | @@ -9,9 +9,13 @@ import { |
| 9 | AuthTerminalOperateScope, | 9 | AuthTerminalOperateScope, |
| 10 | ORCHESTRATION_V2_WS_METHODS, | 10 | ORCHESTRATION_V2_WS_METHODS, |
| 11 | type AuthEnvironmentScope, | 11 | type AuthEnvironmentScope, |
| 12 | EnvironmentAuthorizationError, | ||
| 13 | RpcScopeAuthorization, | ||
| 12 | WS_METHODS, | 14 | WS_METHODS, |
| 13 | WsRpcGroup, | 15 | WsRpcGroup, |
| 14 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 17 | import * as Effect from "effect/Effect"; | ||
| 18 | import * as Layer from "effect/Layer"; | ||
| 15 | import type * as RpcGroup from "effect/unstable/rpc/RpcGroup"; | 19 | import type * as RpcGroup from "effect/unstable/rpc/RpcGroup"; |
| 16 | 20 | ||
| 17 | type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"]; | 21 | type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"]; |
| ... | @@ -212,6 +216,21 @@ export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope | ... | @@ -212,6 +216,21 @@ export function requiredScopeForRpcMethod(method: string): AuthEnvironmentScope |
| 212 | return requiredScope; | 216 | return requiredScope; |
| 213 | } | 217 | } |
| 214 | 218 | ||
| 219 | export const rpcAuthorizationError = (requiredScope: AuthEnvironmentScope) => | ||
| 220 | new EnvironmentAuthorizationError({ | ||
| 221 | message: `The authenticated token is missing required scope: ${requiredScope}.`, | ||
| 222 | requiredScope, | ||
| 223 | }); | ||
| 224 | |||
| 225 | /** Authorizes every RPC on one connection against that connection's session scopes. */ | ||
| 226 | export const rpcScopeAuthorizationLayer = (scopes: ReadonlyArray<AuthEnvironmentScope>) => | ||
| 227 | Layer.succeed(RpcScopeAuthorization)((effect, { rpc }) => { | ||
| 228 | const requiredScope = requiredScopeForRpcMethod(rpc._tag); | ||
| 229 | return scopes.includes(requiredScope) | ||
| 230 | ? effect | ||
| 231 | : Effect.fail(rpcAuthorizationError(requiredScope)); | ||
| 232 | }); | ||
| 233 | |||
| 215 | /** Retrying can install or restart tools even though ordinary listing is readable. */ | 234 | /** Retrying can install or restart tools even though ordinary listing is readable. */ |
| 216 | export const requiredScopeForDeviceList = (input: DeviceListInput): AuthEnvironmentScope => | 235 | export const requiredScopeForDeviceList = (input: DeviceListInput): AuthEnvironmentScope => |
| 217 | input.retryHostId || input.updateTool | 236 | input.retryHostId || input.updateTool |
apps/server/src/mcp/PreviewAutomationBroker.test.ts+9-3| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { expect, it } from "@effect/vitest"; | 2 | import { expect, it } from "@effect/vitest"; |
| 3 | import { | 3 | import { |
| 4 | AuthOrchestrationOperateScope, | ||
| 4 | EnvironmentId, | 5 | EnvironmentId, |
| 5 | PreviewAutomationClientDisconnectedError, | 6 | PreviewAutomationClientDisconnectedError, |
| 6 | PreviewAutomationInvalidSelectorError, | 7 | PreviewAutomationInvalidSelectorError, |
| ... | @@ -17,6 +18,7 @@ import { | ... | @@ -17,6 +18,7 @@ import { |
| 17 | type PreviewAutomationStreamEvent, | 18 | type PreviewAutomationStreamEvent, |
| 18 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 19 | import * as Effect from "effect/Effect"; | 20 | import * as Effect from "effect/Effect"; |
| 21 | import * as Layer from "effect/Layer"; | ||
| 20 | import * as Exit from "effect/Exit"; | 22 | import * as Exit from "effect/Exit"; |
| 21 | import * as Deferred from "effect/Deferred"; | 23 | import * as Deferred from "effect/Deferred"; |
| 22 | import * as Fiber from "effect/Fiber"; | 24 | import * as Fiber from "effect/Fiber"; |
| ... | @@ -27,6 +29,7 @@ import * as TestClock from "effect/testing/TestClock"; | ... | @@ -27,6 +29,7 @@ import * as TestClock from "effect/testing/TestClock"; |
| 27 | import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; | 29 | import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; |
| 28 | import * as RpcTest from "effect/unstable/rpc/RpcTest"; | 30 | import * as RpcTest from "effect/unstable/rpc/RpcTest"; |
| 29 | 31 | ||
| 32 | import { rpcScopeAuthorizationLayer } from "../auth/RpcAuthorization.ts"; | ||
| 30 | import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; | 33 | import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; |
| 31 | 34 | ||
| 32 | const makeBroker = PreviewAutomationBroker.make.pipe(Effect.provide(NodeServices.layer)); | 35 | const makeBroker = PreviewAutomationBroker.make.pipe(Effect.provide(NodeServices.layer)); |
| ... | @@ -1255,9 +1258,12 @@ it.effect("evicts an unanswered host and lets later calls use a healthy runtime" | ... | @@ -1255,9 +1258,12 @@ it.effect("evicts an unanswered host and lets later calls use a healthy runtime" |
| 1255 | ); | 1258 | ); |
| 1256 | const client = yield* RpcTest.makeClient(group).pipe( | 1259 | const client = yield* RpcTest.makeClient(group).pipe( |
| 1257 | Effect.provide( | 1260 | Effect.provide( |
| 1258 | group.toLayer({ | 1261 | Layer.merge( |
| 1259 | [WS_METHODS.previewAutomationConnect]: (host) => Stream.unwrap(broker.connect(host)), | 1262 | group.toLayer({ |
| 1260 | }), | 1263 | [WS_METHODS.previewAutomationConnect]: (host) => Stream.unwrap(broker.connect(host)), |
| 1264 | }), | ||
| 1265 | rpcScopeAuthorizationLayer([AuthOrchestrationOperateScope]), | ||
| 1266 | ), | ||
| 1261 | ), | 1267 | ), |
| 1262 | ); | 1268 | ); |
| 1263 | const events = client[WS_METHODS.previewAutomationConnect](makeHost()); | 1269 | const events = client[WS_METHODS.previewAutomationConnect](makeHost()); |
apps/server/src/ws.ts+12-51| ... | @@ -157,9 +157,9 @@ import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts"; | ... | @@ -157,9 +157,9 @@ import * as ThreadSearch from "./orchestration-v2/ThreadSearch.ts"; |
| 157 | import * as OrchestrationEventStore from "./persistence/Services/OrchestrationEventStore.ts"; | 157 | import * as OrchestrationEventStore from "./persistence/Services/OrchestrationEventStore.ts"; |
| 158 | import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts"; | 158 | import { userFacingDispatchErrorMessage } from "./orchestration-v2/UserFacingErrors.ts"; |
| 159 | import { | 159 | import { |
| 160 | observeRpcEffect as instrumentRpcEffect, | 160 | observeRpcEffect, |
| 161 | observeRpcStream as instrumentRpcStream, | 161 | observeRpcStream, |
| 162 | observeRpcStreamEffect as instrumentRpcStreamEffect, | 162 | observeRpcStreamEffect, |
| 163 | } from "./observability/RpcInstrumentation.ts"; | 163 | } from "./observability/RpcInstrumentation.ts"; |
| 164 | import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; | 164 | import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; |
| 165 | import * as ProviderInstanceRegistry from "./provider/Services/ProviderInstanceRegistry.ts"; | 165 | import * as ProviderInstanceRegistry from "./provider/Services/ProviderInstanceRegistry.ts"; |
| ... | @@ -207,7 +207,11 @@ import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; | ... | @@ -207,7 +207,11 @@ import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; |
| 207 | import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts"; | 207 | import * as RemoteOpenTargets from "./environment/RemoteOpenTargets.ts"; |
| 208 | import * as BackgroundPolicy from "./background/BackgroundPolicy.ts"; | 208 | import * as BackgroundPolicy from "./background/BackgroundPolicy.ts"; |
| 209 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; | 209 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; |
| 210 | import { requiredScopeForRpcMethod, requiredScopeForDeviceList } from "./auth/RpcAuthorization.ts"; | 210 | import { |
| 211 | requiredScopeForDeviceList, | ||
| 212 | rpcAuthorizationError, | ||
| 213 | rpcScopeAuthorizationLayer, | ||
| 214 | } from "./auth/RpcAuthorization.ts"; | ||
| 211 | import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; | 215 | import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; |
| 212 | import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; | 216 | import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; |
| 213 | import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; | 217 | import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; |
| ... | @@ -1309,25 +1313,15 @@ const makeWsRpcLayer = ( | ... | @@ -1309,25 +1313,15 @@ const makeWsRpcLayer = ( |
| 1309 | const processResourceMonitor = yield* ProcessResourceMonitor.ProcessResourceMonitor; | 1313 | const processResourceMonitor = yield* ProcessResourceMonitor.ProcessResourceMonitor; |
| 1310 | const resourceTelemetry = yield* ResourceTelemetry.ResourceTelemetry; | 1314 | const resourceTelemetry = yield* ResourceTelemetry.ResourceTelemetry; |
| 1311 | const relayClient = yield* RelayClient.RelayClient; | 1315 | const relayClient = yield* RelayClient.RelayClient; |
| 1312 | const authorizationError = (requiredScope: AuthEnvironmentScope) => | 1316 | // RpcScopeAuthorization checks each RPC's declared scope before its handler |
| 1313 | new EnvironmentAuthorizationError({ | 1317 | // runs. This covers the one RPC whose scope depends on its input. |
| 1314 | message: `The authenticated token is missing required scope: ${requiredScope}.`, | ||
| 1315 | requiredScope, | ||
| 1316 | }); | ||
| 1317 | const authorizeEffect = <A, E, R>( | 1318 | const authorizeEffect = <A, E, R>( |
| 1318 | requiredScope: AuthEnvironmentScope, | 1319 | requiredScope: AuthEnvironmentScope, |
| 1319 | effect: Effect.Effect<A, E, R>, | 1320 | effect: Effect.Effect<A, E, R>, |
| 1320 | ): Effect.Effect<A, E | EnvironmentAuthorizationError, R> => | 1321 | ): Effect.Effect<A, E | EnvironmentAuthorizationError, R> => |
| 1321 | currentSession.scopes.includes(requiredScope) | 1322 | currentSession.scopes.includes(requiredScope) |
| 1322 | ? effect | 1323 | ? effect |
| 1323 | : Effect.fail(authorizationError(requiredScope)); | 1324 | : Effect.fail(rpcAuthorizationError(requiredScope)); |
| 1324 | const authorizeStream = <A, E, R>( | ||
| 1325 | requiredScope: AuthEnvironmentScope, | ||
| 1326 | stream: Stream.Stream<A, E, R>, | ||
| 1327 | ): Stream.Stream<A, E | EnvironmentAuthorizationError, R> => | ||
| 1328 | currentSession.scopes.includes(requiredScope) | ||
| 1329 | ? stream | ||
| 1330 | : Stream.fail(authorizationError(requiredScope)); | ||
| 1331 | 1325 | ||
| 1332 | const acpRegistryProject = Effect.fn("ws.acpRegistry.project")(function* ( | 1326 | const acpRegistryProject = Effect.fn("ws.acpRegistry.project")(function* ( |
| 1333 | projectId: ProjectId, | 1327 | projectId: ProjectId, |
| ... | @@ -1650,40 +1644,6 @@ const makeWsRpcLayer = ( | ... | @@ -1650,40 +1644,6 @@ const makeWsRpcLayer = ( |
| 1650 | yield* providerRegistry.refreshInstance(input.instanceId); | 1644 | yield* providerRegistry.refreshInstance(input.instanceId); |
| 1651 | return { disabled: true } as const; | 1645 | return { disabled: true } as const; |
| 1652 | }); | 1646 | }); |
| 1653 | const observeRpcEffect = <A, E, R>( | ||
| 1654 | method: string, | ||
| 1655 | effect: Effect.Effect<A, E, R>, | ||
| 1656 | traceAttributes?: Readonly<Record<string, unknown>>, | ||
| 1657 | ) => | ||
| 1658 | instrumentRpcEffect( | ||
| 1659 | method, | ||
| 1660 | authorizeEffect(requiredScopeForRpcMethod(method), effect), | ||
| 1661 | traceAttributes, | ||
| 1662 | ); | ||
| 1663 | const observeRpcStream = <A, E, R>( | ||
| 1664 | method: string, | ||
| 1665 | stream: Stream.Stream<A, E, R>, | ||
| 1666 | traceAttributes?: Readonly<Record<string, unknown>>, | ||
| 1667 | ) => | ||
| 1668 | instrumentRpcStream( | ||
| 1669 | method, | ||
| 1670 | authorizeStream(requiredScopeForRpcMethod(method), stream), | ||
| 1671 | traceAttributes, | ||
| 1672 | ); | ||
| 1673 | const observeRpcStreamEffect = <A, StreamError, StreamContext, EffectError, EffectContext>( | ||
| 1674 | method: string, | ||
| 1675 | effect: Effect.Effect< | ||
| 1676 | Stream.Stream<A, StreamError, StreamContext>, | ||
| 1677 | EffectError, | ||
| 1678 | EffectContext | ||
| 1679 | >, | ||
| 1680 | traceAttributes?: Readonly<Record<string, unknown>>, | ||
| 1681 | ) => | ||
| 1682 | instrumentRpcStreamEffect( | ||
| 1683 | method, | ||
| 1684 | authorizeEffect(requiredScopeForRpcMethod(method), effect), | ||
| 1685 | traceAttributes, | ||
| 1686 | ); | ||
| 1687 | const loadAuthAccessSnapshot = () => | 1647 | const loadAuthAccessSnapshot = () => |
| 1688 | Effect.all({ | 1648 | Effect.all({ |
| 1689 | pairingLinks: serverAuth.listPairingLinks(), | 1649 | pairingLinks: serverAuth.listPairingLinks(), |
| ... | @@ -3842,6 +3802,7 @@ export const websocketRpcRouteLayer = Layer.unwrap( | ... | @@ -3842,6 +3802,7 @@ export const websocketRpcRouteLayer = Layer.unwrap( |
| 3842 | const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket; | 3802 | const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket; |
| 3843 | yield* RpcServer.make(ServerWsRpcGroup, { disableTracing: true }).pipe( | 3803 | yield* RpcServer.make(ServerWsRpcGroup, { disableTracing: true }).pipe( |
| 3844 | Effect.provideService(RpcServer.Protocol, withTerminalOutputWindow(protocol)), | 3804 | Effect.provideService(RpcServer.Protocol, withTerminalOutputWindow(protocol)), |
| 3805 | Effect.provide(rpcScopeAuthorizationLayer(session.scopes)), | ||
| 3845 | Effect.forkScoped, | 3806 | Effect.forkScoped, |
| 3846 | ); | 3807 | ); |
| 3847 | // @effect-diagnostics-next-line returnEffectInGen:off | 3808 | // @effect-diagnostics-next-line returnEffectInGen:off |
docs/internals/environment-auth.md+2-1| ... | @@ -28,7 +28,8 @@ Bearer and DPoP clients obtain short-lived WebSocket tickets through authenticat | ... | @@ -28,7 +28,8 @@ Bearer and DPoP clients obtain short-lived WebSocket tickets through authenticat |
| 28 | HTTP so long-lived tokens stay out of socket URLs. Browser sessions can | 28 | HTTP so long-lived tokens stay out of socket URLs. Browser sessions can |
| 29 | authenticate the upgrade with their cookie. A successful handshake grants no | 29 | authenticate the upgrade with their cookie. A successful handshake grants no |
| 30 | extra authority: [every RPC declares a required | 30 | extra authority: [every RPC declares a required |
| 31 | scope](../../apps/server/src/auth/RpcAuthorization.ts). | 31 | scope](../../apps/server/src/auth/RpcAuthorization.ts), and the WebSocket RPC |
| 32 | group's `RpcScopeAuthorization` middleware checks it before any handler runs. | ||
| 32 | 33 | ||
| 33 | Desktop restarts forget the previous local bearer token, so its reusable | 34 | Desktop restarts forget the previous local bearer token, so its reusable |
| 34 | bootstrap grant replaces earlier sessions for the same subject and method. | 35 | bootstrap grant replaces earlier sessions for the same subject and method. |
packages/contracts/src/rpc.ts+12-1| ... | @@ -9,6 +9,7 @@ import { | ... | @@ -9,6 +9,7 @@ import { |
| 9 | import * as Schema from "effect/Schema"; | 9 | import * as Schema from "effect/Schema"; |
| 10 | import * as Rpc from "effect/unstable/rpc/Rpc"; | 10 | import * as Rpc from "effect/unstable/rpc/Rpc"; |
| 11 | import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; | 11 | import * as RpcGroup from "effect/unstable/rpc/RpcGroup"; |
| 12 | import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware"; | ||
| 12 | import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; | 13 | import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; |
| 13 | import { | 14 | import { |
| 14 | CodexAuthCallbackInput, | 15 | CodexAuthCallbackInput, |
| ... | @@ -1682,6 +1683,16 @@ const WsSubscribeResourceTelemetryRpc = Rpc.make(WS_METHODS.subscribeResourceTel | ... | @@ -1682,6 +1683,16 @@ const WsSubscribeResourceTelemetryRpc = Rpc.make(WS_METHODS.subscribeResourceTel |
| 1682 | stream: true, | 1683 | stream: true, |
| 1683 | }); | 1684 | }); |
| 1684 | 1685 | ||
| 1686 | /** | ||
| 1687 | * Checks the connection's scopes against the scope each RPC declares, before | ||
| 1688 | * the handler runs. Every RPC in `WsRpcGroup` carries it, so a handler cannot | ||
| 1689 | * be added without authorization. | ||
| 1690 | */ | ||
| 1691 | export class RpcScopeAuthorization extends RpcMiddleware.Service<RpcScopeAuthorization>()( | ||
| 1692 | "t3/contracts/RpcScopeAuthorization", | ||
| 1693 | { error: EnvironmentAuthorizationError }, | ||
| 1694 | ) {} | ||
| 1695 | |||
| 1685 | export const WsRpcGroup = RpcGroup.make( | 1696 | export const WsRpcGroup = RpcGroup.make( |
| 1686 | WsServerProbeRpc, | 1697 | WsServerProbeRpc, |
| 1687 | WsServerGetConfigRpc, | 1698 | WsServerGetConfigRpc, |
| ... | @@ -1856,4 +1867,4 @@ export const WsRpcGroup = RpcGroup.make( | ... | @@ -1856,4 +1867,4 @@ export const WsRpcGroup = RpcGroup.make( |
| 1856 | WsOrchestrationV2SubscribeArchivedShellRpc, | 1867 | WsOrchestrationV2SubscribeArchivedShellRpc, |
| 1857 | WsOrchestrationV2SubscribeShellRpc, | 1868 | WsOrchestrationV2SubscribeShellRpc, |
| 1858 | WsOrchestrationV2SubscribeThreadRpc, | 1869 | WsOrchestrationV2SubscribeThreadRpc, |
| 1859 | ); | 1870 | ).middleware(RpcScopeAuthorization); |