1import { ArchiveError } from "@t3tools/contracts";
2import * as ArchiveClient from "./ArchiveClient.ts";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { HttpServerResponse } from "effect/http";
6import {
7 injectMcpAppCookieBootstrap,
8 injectMcpAppThemeBootstrap,
9 MCP_APP_MAX_HTML_BYTES,
10} from "@t3tools/shared/mcpApp";
11
12export const remoteResponse = Effect.fn("ArchiveAssetResponse.remoteResponse")(function* (
13 asset: {
14 readonly location: string;
15 readonly hash: string;
16 readonly mimeType: string;
17 readonly download: boolean;
18 readonly fileName: string;
19 readonly renderIntent?: "mcp-app";
20 },
21 range: string | undefined,
22) {
23 const client = yield* ArchiveClient.ArchiveClient;
24 const result = yield* client.open(asset.location, asset.hash, range);
25 if (result.kind === "file") return result;
26 const response = result.response;
27 const headers: Record<string, string> = {
28 "content-type": asset.mimeType,
29 "cache-control": "private, no-store",
30 "x-content-type-options": "nosniff",
31 };
32 for (const name of ["content-length", "content-range", "accept-ranges"]) {
33 const value = response.headers[name];
34 if (value !== undefined) headers[name] = value;
35 }
36 if (asset.download)
37 headers["content-disposition"] =
38 `attachment; filename*=UTF-8''${encodeURIComponent(asset.fileName)}`;
39 if (asset.mimeType === "image/svg+xml")
40 headers["content-security-policy"] = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
41 if (
42 asset.renderIntent === "mcp-app" &&
43 !asset.download &&
44 asset.mimeType.startsWith("text/html")
45 ) {
46 const parts: Array<Uint8Array> = [];
47 let length = 0;
48 yield* response.stream.pipe(
49 Stream.runForEach((bytes) =>
50 Effect.gen(function* () {
51 length += bytes.length;
52 if (length > MCP_APP_MAX_HTML_BYTES)
53 return yield* new ArchiveError({ reason: "integrity" });
54 parts.push(bytes);
55 }),
56 ),
57 );
58 const bytes = new Uint8Array(length);
59 let offset = 0;
60 for (const part of parts) {
61 bytes.set(part, offset);
62 offset += part.length;
63 }
64 const body = injectMcpAppCookieBootstrap(
65 injectMcpAppThemeBootstrap(new TextDecoder().decode(bytes)),
66 );
67 delete headers["content-length"];
68 return { kind: "response" as const, response: HttpServerResponse.text(body, { headers }) };
69 }
70 return {
71 kind: "response" as const,
72 response: HttpServerResponse.stream(response.stream, { status: response.status, headers }),
73 };
74});