| 1 | import * as Lambda from "@/AWS/Lambda"; |
| 2 | import * as Route53Domains from "@/AWS/Route53Domains"; |
| 3 | import * as Duration from "effect/Duration"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Result from "effect/Result"; |
| 7 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 8 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 9 | import path from "pathe"; |
| 10 | |
| 11 | const main = path.resolve(import.meta.dirname, "handler.ts"); |
| 12 | |
| 13 | export 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. |
| 20 | const 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 | |
| 41 | export 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 | ); |