| 1 | import * as ApiGatewayV2 from "@/AWS/ApiGatewayV2"; |
| 2 | import * as Lambda from "@/AWS/Lambda"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Stream from "effect/Stream"; |
| 6 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 7 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 8 | import path from "pathe"; |
| 9 | |
| 10 | const main = path.resolve(import.meta.dirname, "bindings-handler.ts"); |
| 11 | |
| 12 | export class ApiGatewayV2BindingsFunction extends Lambda.Function<Lambda.Function>()( |
| 13 | "ApiGatewayV2BindingsFunction", |
| 14 | ) {} |
| 15 | |
| 16 | /** |
| 17 | * Fixture Lambda for the ApiGatewayV2 runtime bindings. It owns a minimal |
| 18 | * HTTP API (one `HTTP_PROXY` route + an auto-deployed `$default` stage) so |
| 19 | * the control-plane bindings have a live target, and exposes one route per |
| 20 | * binding: |
| 21 | * |
| 22 | * - `GET /export` → `ExportApi` (OpenAPI 3.0 export) |
| 23 | * - `POST /reset-authorizers` → `ResetAuthorizersCache` |
| 24 | */ |
| 25 | export default ApiGatewayV2BindingsFunction.make( |
| 26 | { |
| 27 | main, |
| 28 | functionUrl: true, |
| 29 | }, |
| 30 | Effect.gen(function* () { |
| 31 | // Minimal HTTP API + route + stage the bindings target. |
| 32 | const api = yield* ApiGatewayV2.Api("AgV2BindingsApi", { |
| 33 | protocolType: "HTTP", |
| 34 | }); |
| 35 | const integration = yield* ApiGatewayV2.Integration( |
| 36 | "AgV2BindingsUpstream", |
| 37 | { |
| 38 | api, |
| 39 | integrationType: "HTTP_PROXY", |
| 40 | integrationUri: "https://example.com", |
| 41 | integrationMethod: "ANY", |
| 42 | payloadFormatVersion: "1.0", |
| 43 | }, |
| 44 | ); |
| 45 | yield* ApiGatewayV2.Route("AgV2BindingsPing", { |
| 46 | api, |
| 47 | routeKey: "GET /ping", |
| 48 | integration, |
| 49 | }); |
| 50 | const stage = yield* ApiGatewayV2.Stage("AgV2BindingsStage", { |
| 51 | api, |
| 52 | autoDeploy: true, |
| 53 | }); |
| 54 | |
| 55 | const exportApi = yield* ApiGatewayV2.ExportApi(api); |
| 56 | const resetAuthorizersCache = |
| 57 | yield* ApiGatewayV2.ResetAuthorizersCache(stage); |
| 58 | |
| 59 | return { |
| 60 | fetch: Effect.gen(function* () { |
| 61 | const request = yield* HttpServerRequest; |
| 62 | const url = new URL(request.originalUrl); |
| 63 | const pathname = url.pathname; |
| 64 | |
| 65 | if (request.method === "GET" && pathname === "/export") { |
| 66 | const exported = yield* exportApi({ OutputType: "JSON" }).pipe( |
| 67 | Effect.orDie, |
| 68 | ); |
| 69 | const document = exported.body |
| 70 | ? yield* Stream.mkString(Stream.decodeText(exported.body)).pipe( |
| 71 | Effect.orDie, |
| 72 | ) |
| 73 | : ""; |
| 74 | const spec = yield* Effect.try( |
| 75 | () => |
| 76 | JSON.parse(document) as { |
| 77 | openapi?: string; |
| 78 | paths?: Record<string, unknown>; |
| 79 | }, |
| 80 | ).pipe(Effect.orDie); |
| 81 | return yield* HttpServerResponse.json({ |
| 82 | openapi: spec.openapi, |
| 83 | paths: Object.keys(spec.paths ?? {}), |
| 84 | }); |
| 85 | } |
| 86 | |
| 87 | if (request.method === "POST" && pathname === "/reset-authorizers") { |
| 88 | // The stage has no Lambda-authorizer cache, so API Gateway may |
| 89 | // answer with a typed NotFound — the IAM grant is still |
| 90 | // exercised (a missing grant surfaces as an auth error instead, |
| 91 | // failing the route with a 500). |
| 92 | const flushed = yield* resetAuthorizersCache().pipe( |
| 93 | Effect.map(() => true), |
| 94 | Effect.catchTag("NotFoundException", () => Effect.succeed(false)), |
| 95 | Effect.orDie, |
| 96 | ); |
| 97 | return yield* HttpServerResponse.json({ flushed }); |
| 98 | } |
| 99 | |
| 100 | return yield* HttpServerResponse.json( |
| 101 | { error: "Not found", method: request.method, path: pathname }, |
| 102 | { status: 404 }, |
| 103 | ); |
| 104 | }).pipe(Effect.orDie), |
| 105 | }; |
| 106 | }).pipe( |
| 107 | Effect.provide( |
| 108 | Layer.mergeAll( |
| 109 | ApiGatewayV2.ExportApiHttp, |
| 110 | ApiGatewayV2.ResetAuthorizersCacheHttp, |
| 111 | ), |
| 112 | ), |
| 113 | ), |
| 114 | ); |