1import {
2 AuthAccessReadScope,
3 AuthAccessWriteScope,
4 AuthEnvironmentConnectScope,
5 HostedSourceConnection,
6 HubError,
7 SelfHostedConnectionStatus,
8} from "@t3tools/contracts";
9import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";
13import * as Schema from "effect/Schema";
14import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
15import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
16import * as OutboundHub from "./OutboundHub.ts";
17import * as SourceConnection from "./SourceConnection.ts";
18
19const isHubError = Schema.is(HubError);
20const statusResponse = HttpServerResponse.schemaJson(SelfHostedConnectionStatus);
21
22const errors = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
23 effect.pipe(
24 Effect.catch((error) =>
25 Effect.succeed(
26 HttpServerResponse.empty({
27 status: EnvironmentAuth.isServerAuthCredentialError(error)
28 ? 401
29 : Schema.isSchemaError(error)
30 ? 400
31 : isHubError(error) && error.reason === "credential"
32 ? 403
33 : isHubError(error) && error.reason === "address"
34 ? 400
35 : 503,
36 }),
37 ),
38 ),
39 );
40
41const connection = Effect.gen(function* () {
42 const request = yield* HttpServerRequest.HttpServerRequest;
43 const auth = yield* EnvironmentAuth.EnvironmentAuth;
44 const session = yield* auth.authenticateHttpRequest(request);
45 const source = yield* SourceConnection.SourceConnection;
46 if (request.method === "GET") {
47 if (!session.scopes.includes(AuthAccessReadScope))
48 return HttpServerResponse.empty({ status: 403 });
49 const current = yield* source.current;
50 return yield* statusResponse(
51 {
52 configured: Option.isSome(current),
53 connected: yield* source.connected,
54 expiresAt: Option.isSome(current) ? DateTime.formatIso(current.value.expiresAt) : null,
55 },
56 { headers: { "cache-control": "no-store" } },
57 );
58 }
59 if (
60 session.subject !== "desktop-bootstrap" ||
61 session.parentSessionId !== undefined ||
62 !session.scopes.includes(AuthAccessWriteScope)
63 ) {
64 return HttpServerResponse.empty({ status: 403 });
65 }
66 yield* source.update(
67 request.method === "DELETE"
68 ? undefined
69 : yield* HttpServerRequest.schemaBodyJson(Schema.toCodecJson(HostedSourceConnection)),
70 );
71 return HttpServerResponse.empty({ status: 204 });
72});
73
74const outbound = Effect.gen(function* () {
75 const request = yield* HttpServerRequest.HttpServerRequest;
76 const session = yield* (yield* EnvironmentAuth.EnvironmentAuth).authenticateWebSocketUpgrade(
77 request,
78 );
79 const hub = yield* OutboundHub.OutboundHub;
80 const params = yield* HttpRouter.params;
81 if (!session.deviceId || !session.scopes.includes(AuthEnvironmentConnectScope))
82 return HttpServerResponse.empty({ status: 403 });
83 const socket = yield* request.upgrade;
84 if (params.channelId) yield* hub.channel(session, params.channelId, socket);
85 else yield* hub.accept(session, socket);
86 return HttpServerResponse.empty();
87});
88
89export const layer = Layer.mergeAll(
90 HttpRouter.add("GET", "/api/self-hosted/connection", connection.pipe(errors)),
91 HttpRouter.add("POST", "/api/self-hosted/connection", connection.pipe(errors)),
92 HttpRouter.add("DELETE", "/api/self-hosted/connection", connection.pipe(errors)),
93 HttpRouter.add("GET", "/api/hub/outbound", outbound.pipe(errors)),
94 HttpRouter.add("GET", "/api/hub/outbound/channel/:channelId", outbound.pipe(errors)),
95);