1import * as Lambda from "@/AWS/Lambda";
2import * as MQ from "@/AWS/MQ";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Redacted from "effect/Redacted";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
11
12export class MQBindingsFunction extends Lambda.Function<Lambda.Function>()(
13 "MQBindingsFunction",
14) {}
15
16export default MQBindingsFunction.make(
17 {
18 main,
19 functionUrl: true,
20 },
21 Effect.gen(function* () {
22 // A single-instance mq.t3.micro ActiveMQ broker is the cheapest topology
23 // and the only engine whose user APIs (CreateUser/UpdateUser/...) are
24 // supported. Omitting engineVersion lets AWS pick the current default.
25 const broker = yield* MQ.Broker("BindingsBroker", {
26 engineType: "ACTIVEMQ",
27 hostInstanceType: "mq.t3.micro",
28 deploymentMode: "SINGLE_INSTANCE",
29 publiclyAccessible: true,
30 users: [
31 {
32 username: "alchemyadmin",
33 password: Redacted.make("SuperSecretPassw0rd!"),
34 },
35 ],
36 });
37
38 const describeBroker = yield* MQ.DescribeBroker(broker);
39 const rebootBroker = yield* MQ.RebootBroker(broker);
40 const promote = yield* MQ.Promote(broker);
41 const createUser = yield* MQ.CreateUser(broker);
42 const updateUser = yield* MQ.UpdateUser(broker);
43 const deleteUser = yield* MQ.DeleteUser(broker);
44 const describeUser = yield* MQ.DescribeUser(broker);
45 const listUsers = yield* MQ.ListUsers(broker);
46 const listBrokers = yield* MQ.ListBrokers();
47
48 const bound = {
49 describeBroker,
50 rebootBroker,
51 promote,
52 createUser,
53 updateUser,
54 deleteUser,
55 describeUser,
56 listUsers,
57 listBrokers,
58 };
59
60 return {
61 fetch: Effect.gen(function* () {
62 const request = yield* HttpServerRequest;
63 const url = new URL(request.originalUrl);
64 const pathname = url.pathname;
65
66 if (request.method === "GET" && pathname === "/bindings") {
67 return yield* HttpServerResponse.json({
68 bound: Object.keys(bound),
69 });
70 }
71
72 if (request.method === "GET" && pathname === "/broker") {
73 // BrokerId injection scopes the call to the bound broker.
74 const response = yield* describeBroker();
75 return yield* HttpServerResponse.json({
76 brokerName: response.BrokerName,
77 brokerState: response.BrokerState,
78 });
79 }
80
81 if (request.method === "GET" && pathname === "/brokers") {
82 const response = yield* listBrokers();
83 return yield* HttpServerResponse.json({
84 count: (response.BrokerSummaries ?? []).length,
85 });
86 }
87
88 if (request.method === "GET" && pathname === "/users") {
89 const response = yield* listUsers();
90 return yield* HttpServerResponse.json({
91 count: (response.Users ?? []).length,
92 });
93 }
94
95 if (request.method === "POST" && pathname === "/users") {
96 // Full staged-user round-trip: create stages a CREATE pending
97 // change, describe reads it back, delete stages the removal — the
98 // broker never needs a reboot, so the test stays fast and the
99 // stack destroys cleanly.
100 yield* createUser({
101 Username: "alchemytenant",
102 Password: Redacted.make("AnotherSecretPassw0rd!"),
103 });
104 const described = yield* describeUser({
105 Username: "alchemytenant",
106 });
107 yield* deleteUser({ Username: "alchemytenant" });
108 return yield* HttpServerResponse.json({
109 created: true,
110 describedUsername: described.Username,
111 pendingChange: described.Pending?.PendingChange,
112 deleted: true,
113 });
114 }
115
116 if (
117 request.method === "POST" &&
118 pathname === "/users/typed-not-found"
119 ) {
120 // UpdateUser on a nonexistent user round-trips the typed
121 // NotFoundException — an IAM gap would surface AccessDenied (500),
122 // so the typed tag proves grant + injection end-to-end.
123 const typed = yield* updateUser({
124 Username: "alchemynonexistentuser",
125 ConsoleAccess: true,
126 }).pipe(
127 Effect.map(() => false),
128 Effect.catchTag("NotFoundException", () => Effect.succeed(true)),
129 );
130 return yield* HttpServerResponse.json({ typed });
131 }
132
133 if (request.method === "POST" && pathname === "/promote") {
134 // The bound broker has no CRDR replication, so Promote returns the
135 // typed BadRequestException — proving the grant without touching
136 // broker state.
137 const typed = yield* promote({ Mode: "SWITCHOVER" }).pipe(
138 Effect.map(() => false),
139 Effect.catchTag("BadRequestException", () => Effect.succeed(true)),
140 );
141 return yield* HttpServerResponse.json({ typed });
142 }
143
144 if (request.method === "POST" && pathname === "/reboot") {
145 // A real reboot of the RUNNING broker — call this from the LAST
146 // test; the Broker provider's delete waits for the broker to
147 // settle before deleting.
148 yield* rebootBroker();
149 return yield* HttpServerResponse.json({ rebooting: true });
150 }
151
152 return yield* HttpServerResponse.json(
153 { error: "Not found", method: request.method, pathname },
154 { status: 404 },
155 );
156 }).pipe(Effect.orDie),
157 };
158 }).pipe(
159 Effect.provide(
160 Layer.mergeAll(
161 MQ.DescribeBrokerHttp,
162 MQ.RebootBrokerHttp,
163 MQ.PromoteHttp,
164 MQ.CreateUserHttp,
165 MQ.UpdateUserHttp,
166 MQ.DeleteUserHttp,
167 MQ.DescribeUserHttp,
168 MQ.ListUsersHttp,
169 MQ.ListBrokersHttp,
170 ),
171 ),
172 ),
173);