1import * as Lambda from "@/AWS/Lambda";
2import * as Route53Domains from "@/AWS/Route53Domains";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Result from "effect/Result";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "handler.ts");
12
13export class Route53DomainsTestFunction extends Lambda.Function<Lambda.Function>()(
14 "Route53DomainsTestFunction",
15) {}
16
17// Serialize a Result into { ok, errorTag } JSON — routes that exercise
18// error paths must reach the API (proving the IAM grant and the us-east-1
19// pin) and fail with a typed domain-level error, never AccessDenied.
20const errorRoute = <A, E extends { _tag: string }>(
21 effect: Effect.Effect<A, E>,
22 onSuccess: (value: A) => object,
23) =>
24 Effect.gen(function* () {
25 const result = yield* Effect.result(effect);
26 if (Result.isFailure(result)) {
27 return yield* HttpServerResponse.json({
28 ok: false,
29 errorTag: result.failure._tag,
30 errorMessage: String(
31 (result.failure as { message?: unknown }).message ?? "",
32 ),
33 });
34 }
35 return yield* HttpServerResponse.json({
36 ok: true,
37 ...onSuccess(result.success),
38 });
39 });
40
41export default Route53DomainsTestFunction.make(
42 {
43 main,
44 functionUrl: true,
45 // Route 53 Domains calls cross to us-east-1; give cold starts headroom
46 // over the 3s default.
47 timeout: Duration.seconds(30),
48 },
49 Effect.gen(function* () {
50 const checkDomainAvailability =
51 yield* Route53Domains.CheckDomainAvailability();
52 const checkDomainTransferability =
53 yield* Route53Domains.CheckDomainTransferability();
54 const getDomainDetail = yield* Route53Domains.GetDomainDetail();
55 const getDomainSuggestions = yield* Route53Domains.GetDomainSuggestions();
56 const getOperationDetail = yield* Route53Domains.GetOperationDetail();
57 const listDomains = yield* Route53Domains.ListDomains();
58 const listOperations = yield* Route53Domains.ListOperations();
59 const listPrices = yield* Route53Domains.ListPrices();
60 const registerDomain = yield* Route53Domains.RegisterDomain();
61 const renewDomain = yield* Route53Domains.RenewDomain();
62 const retrieveDomainAuthCode =
63 yield* Route53Domains.RetrieveDomainAuthCode();
64 const updateDomainNameservers =
65 yield* Route53Domains.UpdateDomainNameservers();
66
67 return {
68 fetch: Effect.gen(function* () {
69 const request = yield* HttpServerRequest;
70 const url = new URL(request.originalUrl);
71 const pathname = url.pathname;
72
73 // Cheap readiness route — no Route 53 Domains call.
74 if (request.method === "GET" && pathname === "/ping") {
75 return yield* HttpServerResponse.json({ ok: true });
76 }
77
78 if (request.method === "GET" && pathname === "/availability") {
79 const result = yield* checkDomainAvailability({
80 // Deterministic probe name — availability checks are read-only
81 // and free; this suite NEVER registers a domain.
82 DomainName: "alchemy-effect-r53d-probe-a32892.com",
83 });
84 return yield* HttpServerResponse.json({
85 availability: result.Availability,
86 });
87 }
88
89 if (request.method === "GET" && pathname === "/transferability") {
90 const result = yield* checkDomainTransferability({
91 DomainName: "example.com",
92 });
93 return yield* HttpServerResponse.json({
94 transferable: result.Transferability?.Transferable,
95 });
96 }
97
98 if (request.method === "GET" && pathname === "/suggestions") {
99 const result = yield* getDomainSuggestions({
100 DomainName: "example.com",
101 SuggestionCount: 5,
102 OnlyAvailable: false,
103 });
104 return yield* HttpServerResponse.json({
105 count: (result.SuggestionsList ?? []).length,
106 });
107 }
108
109 if (request.method === "GET" && pathname === "/prices") {
110 const result = yield* listPrices({ Tld: "com" });
111 return yield* HttpServerResponse.json({
112 count: (result.Prices ?? []).length,
113 registrationPrice: result.Prices?.[0]?.RegistrationPrice?.Price,
114 });
115 }
116
117 if (request.method === "GET" && pathname === "/operations") {
118 const result = yield* listOperations({ MaxItems: 10 });
119 return yield* HttpServerResponse.json({
120 count: (result.Operations ?? []).length,
121 });
122 }
123
124 if (request.method === "GET" && pathname === "/domains") {
125 const result = yield* listDomains({ MaxItems: 100 });
126 return yield* HttpServerResponse.json({
127 count: (result.Domains ?? []).length,
128 names: (result.Domains ?? [])
129 .map((domain) => domain.DomainName)
130 .filter((name) => name !== undefined),
131 });
132 }
133
134 if (request.method === "GET" && pathname === "/detail") {
135 // example.com is never registered in the test account — the call
136 // must reach the API (proving the IAM grant and us-east-1 pin)
137 // and come back as a typed domain-level error, not AccessDenied.
138 return yield* errorRoute(
139 getDomainDetail({ DomainName: "example.com" }),
140 (detail) => ({ domainName: detail.DomainName }),
141 );
142 }
143
144 if (request.method === "GET" && pathname === "/operation-detail") {
145 // A syntactically valid but non-existent operation id — the call
146 // must reach the API and fail with a typed InvalidInput, never
147 // AccessDenied.
148 return yield* errorRoute(
149 getOperationDetail({
150 OperationId: "00000000-0000-0000-0000-000000000000",
151 }),
152 (detail) => ({ status: detail.Status }),
153 );
154 }
155
156 if (request.method === "GET" && pathname === "/auth-code") {
157 // example.com is not in the account — typed domain-level error.
158 // On success the AuthCode would be Redacted; this route never
159 // returns it.
160 return yield* errorRoute(
161 retrieveDomainAuthCode({ DomainName: "example.com" }),
162 () => ({ hasAuthCode: true }),
163 );
164 }
165
166 if (request.method === "GET" && pathname === "/nameservers") {
167 // example.com is not in the account — typed domain-level error.
168 return yield* errorRoute(
169 updateDomainNameservers({
170 DomainName: "example.com",
171 Nameservers: [{ Name: "ns-1.awsdns-01.org" }],
172 }),
173 (r) => ({ operationId: r.OperationId }),
174 );
175 }
176
177 if (request.method === "GET" && pathname === "/renew") {
178 // example.com is not in the account — typed domain-level error.
179 // No renewal is ever billed by this suite.
180 return yield* errorRoute(
181 renewDomain({
182 DomainName: "example.com",
183 DurationInYears: 1,
184 CurrentExpiryYear: 2030,
185 }),
186 (r) => ({ operationId: r.OperationId }),
187 );
188 }
189
190 if (request.method === "GET" && pathname === "/register") {
191 // Deliberately unsupported TLD — the request reaches the API and
192 // is rejected with a typed error before any registration or
193 // billing can occur. This suite NEVER registers a domain.
194 return yield* errorRoute(
195 registerDomain({
196 DomainName: "alchemy-effect-r53d-probe-a32892.invalidtld99",
197 DurationInYears: 1,
198 AdminContact: {},
199 RegistrantContact: {},
200 TechContact: {},
201 }),
202 (r) => ({ operationId: r.OperationId }),
203 );
204 }
205
206 return yield* HttpServerResponse.json(
207 { error: "Not found", method: request.method, pathname },
208 { status: 404 },
209 );
210 }).pipe(Effect.orDie),
211 };
212 }).pipe(
213 Effect.provide(
214 Layer.mergeAll(
215 Route53Domains.CheckDomainAvailabilityHttp,
216 Route53Domains.CheckDomainTransferabilityHttp,
217 Route53Domains.GetDomainDetailHttp,
218 Route53Domains.GetDomainSuggestionsHttp,
219 Route53Domains.GetOperationDetailHttp,
220 Route53Domains.ListDomainsHttp,
221 Route53Domains.ListOperationsHttp,
222 Route53Domains.ListPricesHttp,
223 Route53Domains.RegisterDomainHttp,
224 Route53Domains.RenewDomainHttp,
225 Route53Domains.RetrieveDomainAuthCodeHttp,
226 Route53Domains.UpdateDomainNameserversHttp,
227 ),
228 ),
229 ),
230);