1import {
2 AuthOrchestrationOperateScope,
3 HubError,
4 HubInboxCommand,
5 HubOrganizationCommand,
6 HubInbox as HubInboxSchema,
7 HubInboxEnvironment,
8} from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10import * as Crypto from "effect/Crypto";
11import * as Hex from "effect/encoding/Hex";
12import * as Layer from "effect/Layer";
13import * as Schema from "effect/Schema";
14import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
15import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
16import * as HubInbox from "./HubInbox.ts";
17import * as Orchestrator from "../orchestration-v2/Orchestrator.ts";
18
19const isHubError = Schema.is(HubError);
20const isInboxError = Schema.is(HubInbox.HubInboxError);
21const isDispatchError = Schema.is(Orchestrator.OrchestratorDispatchError);
22const encodeInbox = Schema.encodeEffect(Schema.fromJsonString(Schema.toCodecJson(HubInboxSchema)));
23const encodeEnvironment = Schema.encodeEffect(Schema.toCodecJson(HubInboxEnvironment));
24
25const authenticate = Effect.gen(function* () {
26 const auth = yield* EnvironmentAuth.EnvironmentAuth;
27 return yield* auth.authenticateWebSocketUpgrade(yield* HttpServerRequest.HttpServerRequest);
28});
29
30const errors = <E, R>(effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>) =>
31 effect.pipe(
32 Effect.catch((error) => {
33 if (EnvironmentAuth.isServerAuthCredentialError(error))
34 return Effect.succeed(HttpServerResponse.empty({ status: 401 }));
35 if (isHubError(error))
36 return Effect.succeed(
37 HttpServerResponse.jsonUnsafe(
38 { message: error.message, reason: error.reason },
39 {
40 status:
41 error.reason === "credential"
42 ? 403
43 : error.reason === "configuration" || error.reason === "not-found"
44 ? 404
45 : 502,
46 },
47 ),
48 );
49 if (isInboxError(error))
50 return Effect.succeed(
51 HttpServerResponse.jsonUnsafe(
52 { message: error.message, reason: error.reason },
53 {
54 status:
55 error.reason === "thread-missing"
56 ? 404
57 : error.reason === "thread-state"
58 ? 409
59 : 500,
60 },
61 ),
62 );
63 if (Schema.isSchemaError(error))
64 return Effect.succeed(HttpServerResponse.empty({ status: 400 }));
65 if (isDispatchError(error))
66 return Effect.succeed(
67 HttpServerResponse.jsonUnsafe(
68 { message: new HubInbox.HubInboxError({ reason: "thread-state" }).message },
69 { status: 409 },
70 ),
71 );
72 return Effect.succeed(HttpServerResponse.empty({ status: 500 }));
73 }),
74 );
75
76export const layer = Layer.mergeAll(
77 HttpRouter.add(
78 "GET",
79 "/api/hub/inbox",
80 Effect.gen(function* () {
81 const value = yield* (yield* HubInbox.HubInbox).read(yield* authenticate);
82 const body = yield* encodeInbox(value);
83 const digest = yield* (yield* Crypto.Crypto).digest(
84 "SHA-256",
85 new TextEncoder().encode(body),
86 );
87 const etag = `"${Hex.encode(digest)}"`;
88 const request = yield* HttpServerRequest.HttpServerRequest;
89 const headers = { etag, "cache-control": "no-store" };
90 const unchanged = request.headers["if-none-match"]?.split(",").some((value) => {
91 const candidate = value.trim();
92 return candidate === "*" || candidate.replace(/^W\//, "") === etag;
93 });
94 return unchanged
95 ? HttpServerResponse.empty({ status: 304, headers })
96 : HttpServerResponse.text(body, {
97 headers: { ...headers, "content-type": "application/json" },
98 });
99 }).pipe(errors),
100 ),
101 HttpRouter.add(
102 "POST",
103 "/api/hub/inbox",
104 Effect.gen(function* () {
105 const session = yield* authenticate;
106 const input = yield* HttpServerRequest.schemaBodyJson(HubInboxCommand);
107 const value = yield* (yield* HubInbox.HubInbox).organize(input, session);
108 return HttpServerResponse.jsonUnsafe(yield* encodeEnvironment(value), {
109 headers: { "cache-control": "no-store" },
110 });
111 }).pipe(errors),
112 ),
113 HttpRouter.add(
114 "POST",
115 "/api/hub/organization",
116 Effect.gen(function* () {
117 const session = yield* authenticate;
118 if (!session.scopes.includes(AuthOrchestrationOperateScope))
119 return yield* new HubError({ reason: "credential" });
120 const command = yield* HttpServerRequest.schemaBodyJson(HubOrganizationCommand);
121 const result = yield* (yield* Orchestrator.OrchestratorV2).dispatch(command);
122 return HttpServerResponse.jsonUnsafe(
123 { sequence: result.sequence },
124 { headers: { "cache-control": "no-store" } },
125 );
126 }).pipe(errors),
127 ),
128);