1import { Endpoint } from "@distilled.cloud/aws";
2import * as mgmt from "@distilled.cloud/aws/apigatewaymanagementapi";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Binding from "../../Binding.ts";
6import { isBindingHost } from "../Lambda/Function.ts";
7import {
8 ManageConnections,
9 type DeleteConnectionRequest,
10 type GetConnectionRequest,
11 type PostToConnectionRequest,
12} from "./ManageConnections.ts";
13import type { ApiGatewayV2Stage } from "./Stage.ts";
14
15/**
16 * HTTP implementation of the {@link ManageConnections} binding. Signs
17 * `@connections` requests with the Lambda's IAM role against the stage's
18 * callback endpoint.
19 *
20 * Provide it on the hosting Lambda function's Effect so the binding is
21 * available at runtime:
22 *
23 * @example
24 * ```typescript
25 * export default MyFunction.make(
26 * { main: import.meta.url },
27 * Effect.gen(function* () {
28 * const connections = yield* ApiGatewayV2.ManageConnections(stage);
29 * // ... register WebSocket routes that push via `connections`
30 * return {};
31 * }).pipe(Effect.provide(ApiGatewayV2.ManageConnectionsHttp)),
32 * );
33 * ```
34 */
35export const ManageConnectionsHttp = Layer.effect(
36 ManageConnections,
37 Effect.gen(function* () {
38 const postToConnection = yield* mgmt.postToConnection;
39 const getConnection = yield* mgmt.getConnection;
40 const deleteConnection = yield* mgmt.deleteConnection;
41
42 return Effect.fn(function* <S extends ApiGatewayV2Stage>(stage: S) {
43 const CallbackUrl = yield* stage.callbackUrl;
44 if (!globalThis.__ALCHEMY_RUNTIME__) {
45 const host = yield* Binding.Host;
46 if (isBindingHost(host)) {
47 yield* host.bind`Allow(${host}, AWS.ApiGatewayV2.ManageConnections(${stage}))`(
48 {
49 policyStatements: [
50 {
51 Effect: "Allow",
52 Action: ["execute-api:ManageConnections"],
53 Resource: [stage.connectionsArn],
54 },
55 ],
56 },
57 );
58 }
59 }
60
61 // The management API is served from the stage's own callback
62 // endpoint rather than the service default, so each call overrides
63 // the distilled endpoint resolver with the bound URL.
64 const withEndpoint = <A, E>(
65 effect: Effect.Effect<A, E>,
66 endpoint: string,
67 ): Effect.Effect<A, E> =>
68 Effect.provideService(
69 effect,
70 Endpoint.Endpoint,
71 Effect.succeed(endpoint),
72 );
73
74 return {
75 postToConnection: Effect.fn(
76 `AWS.ApiGatewayV2.PostToConnection(${stage.LogicalId})`,
77 )(function* (request: PostToConnectionRequest) {
78 const endpoint = yield* CallbackUrl;
79 return yield* withEndpoint(postToConnection(request), endpoint);
80 }),
81 getConnection: Effect.fn(
82 `AWS.ApiGatewayV2.GetConnection(${stage.LogicalId})`,
83 )(function* (request: GetConnectionRequest) {
84 const endpoint = yield* CallbackUrl;
85 return yield* withEndpoint(getConnection(request), endpoint);
86 }),
87 deleteConnection: Effect.fn(
88 `AWS.ApiGatewayV2.DeleteConnection(${stage.LogicalId})`,
89 )(function* (request: DeleteConnectionRequest) {
90 const endpoint = yield* CallbackUrl;
91 return yield* withEndpoint(deleteConnection(request), endpoint);
92 }),
93 };
94 });
95 }),
96);