1import * as Fly from "@/Fly";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Redacted from "effect/Redacted";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7
8export const CERT_API_PORT = 3000;
9
10export const CertSite = Fly.App("CertSite", { enableSubdomains: true });
11
12export const CertIp = Fly.IpAssignment("CertShared", {
13 app: CertSite,
14 type: "shared_v4",
15});
16
17/**
18 * HTTP Service exercising {@link Fly.WriteCertificates}: one route per
19 * operation, the hostname from the query string, PEMs in the JSON body.
20 */
21export default class CertificatesApi extends Fly.Service<CertificatesApi>()(
22 "CertificatesApi",
23 {
24 app: CertSite,
25 main: import.meta.url,
26 region: "iad",
27 port: CERT_API_PORT,
28 guest: { cpuKind: "shared", cpus: 1, memoryMb: 256 },
29 },
30 Effect.gen(function* () {
31 const certs = yield* Fly.WriteCertificates(CertSite);
32
33 return {
34 fetch: Effect.gen(function* () {
35 const request = yield* HttpServerRequest;
36 const url = new URL(request.url, "http://service");
37 const hostname = url.searchParams.get("hostname") ?? "";
38
39 // `suspend` turns a synchronous throw inside the client call into a
40 // defect the cause handler below can report.
41 const respond = <A, E, R>(make: () => Effect.Effect<A, E, R>) =>
42 Effect.suspend(make).pipe(
43 Effect.flatMap((value) =>
44 HttpServerResponse.json({ ok: true, value: value ?? null }),
45 ),
46 Effect.catchCause((cause) =>
47 HttpServerResponse.json(
48 { ok: false, error: Cause.pretty(cause) },
49 { status: 500 },
50 ),
51 ),
52 );
53
54 switch (url.pathname) {
55 case "/health":
56 return yield* HttpServerResponse.json({ ok: true, version: 2 });
57 case "/request":
58 return yield* respond(() => certs.request(hostname));
59 case "/upload": {
60 const body = (yield* request.json) as {
61 hostname: string;
62 fullchain: string;
63 privateKey: string;
64 };
65 return yield* respond(() =>
66 certs.upload({
67 hostname: body.hostname,
68 fullchain: body.fullchain,
69 privateKey: Redacted.make(body.privateKey),
70 }),
71 );
72 }
73 case "/check":
74 return yield* respond(() => certs.check(hostname));
75 case "/get":
76 return yield* respond(() => certs.get(hostname));
77 case "/remove":
78 return yield* respond(() => certs.remove(hostname));
79 default:
80 return HttpServerResponse.text("not found", { status: 404 });
81 }
82 }),
83 };
84 }).pipe(Effect.provide(Fly.WriteCertificatesHttp)),
85) {}