1import * as Lambda from "@/AWS/Lambda";
2import * as RePostSpace from "@/AWS/RePostSpace";
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
12// A syntactically-valid but nonexistent IAM Identity Center accessor id.
13const BOGUS_ACCESSOR_ID = "00000000-0000-0000-0000-000000000000";
14
15/** Unwrap a distilled sensitive value into its plain string. */
16const unwrapSensitive = (
17 value: string | Redacted.Redacted<string> | undefined,
18): string | undefined =>
19 value === undefined
20 ? undefined
21 : Redacted.isRedacted(value)
22 ? Redacted.value(value)
23 : value;
24
25export class RePostSpaceBindingsFunction extends Lambda.Function<Lambda.Function>()(
26 "RePostSpaceBindingsFunction",
27) {}
28
29export default RePostSpaceBindingsFunction.make(
30 {
31 main,
32 functionUrl: true,
33 },
34 Effect.gen(function* () {
35 const space = yield* RePostSpace.Space("BindingsSpace", {
36 // subdomain must be globally unique across re:Post Private —
37 // deterministic, alchemy-branded constant.
38 subdomain: "alchemy-e2e-repost-bindings",
39 tier: "BASIC",
40 description: "alchemy repostspace bindings fixture",
41 tags: { fixture: "repostspace-bindings" },
42 });
43
44 const sendInvites = yield* RePostSpace.SendInvites(space);
45 const registerAdmin = yield* RePostSpace.RegisterAdmin(space);
46 const deregisterAdmin = yield* RePostSpace.DeregisterAdmin(space);
47 const batchAddRole = yield* RePostSpace.BatchAddRole(space);
48 const batchRemoveRole = yield* RePostSpace.BatchRemoveRole(space);
49 const createChannel = yield* RePostSpace.CreateChannel(space);
50 const getChannel = yield* RePostSpace.GetChannel(space);
51 const listChannels = yield* RePostSpace.ListChannels(space);
52 const updateChannel = yield* RePostSpace.UpdateChannel(space);
53 const addChannelRole =
54 yield* RePostSpace.BatchAddChannelRoleToAccessors(space);
55 const removeChannelRole =
56 yield* RePostSpace.BatchRemoveChannelRoleFromAccessors(space);
57
58 const bound = {
59 sendInvites,
60 registerAdmin,
61 deregisterAdmin,
62 batchAddRole,
63 batchRemoveRole,
64 createChannel,
65 getChannel,
66 listChannels,
67 updateChannel,
68 addChannelRole,
69 removeChannelRole,
70 };
71
72 return {
73 fetch: Effect.gen(function* () {
74 const request = yield* HttpServerRequest;
75 const url = new URL(request.originalUrl);
76 const pathname = url.pathname;
77
78 if (request.method === "GET" && pathname === "/bindings") {
79 return yield* HttpServerResponse.json({
80 bound: Object.keys(bound),
81 });
82 }
83
84 if (request.method === "GET" && pathname === "/channels") {
85 const response = yield* listChannels();
86 return yield* HttpServerResponse.json({
87 count: (response.channels ?? []).length,
88 });
89 }
90
91 if (request.method === "POST" && pathname === "/channels") {
92 // Create → read → rename round-trip; channels cannot be deleted,
93 // but the whole space is destroyed at teardown so nothing leaks.
94 const { channelId } = yield* createChannel({
95 channelName: "alchemy-bindings-channel",
96 channelDescription: "created by the alchemy bindings fixture",
97 });
98 const read = yield* getChannel({ channelId });
99 yield* updateChannel({
100 channelId,
101 channelName: "alchemy-bindings-channel-renamed",
102 });
103 const reread = yield* getChannel({ channelId });
104 return yield* HttpServerResponse.json({
105 channelId,
106 name: unwrapSensitive(read.channelName),
107 renamed: unwrapSensitive(reread.channelName),
108 });
109 }
110
111 if (request.method === "POST" && pathname === "/roles/bogus") {
112 // Batch role APIs report per-accessor failures in the output's
113 // `errors` list (or fail with a typed 400 for malformed input) —
114 // either shape proves the grant + spaceId injection end-to-end.
115 const added = yield* batchAddRole({
116 accessorIds: [BOGUS_ACCESSOR_ID],
117 role: "EXPERT",
118 }).pipe(
119 Effect.map((r) => ({
120 ok: true,
121 errors: r.errors.length,
122 added: r.addedAccessorIds.length,
123 })),
124 Effect.catchTag(
125 ["ValidationException", "ResourceNotFoundException"],
126 (e) => Effect.succeed({ ok: false, tag: e._tag }),
127 ),
128 );
129 const removed = yield* batchRemoveRole({
130 accessorIds: [BOGUS_ACCESSOR_ID],
131 role: "EXPERT",
132 }).pipe(
133 Effect.map((r) => ({
134 ok: true,
135 errors: r.errors.length,
136 removed: r.removedAccessorIds.length,
137 })),
138 Effect.catchTag(
139 ["ValidationException", "ResourceNotFoundException"],
140 (e) => Effect.succeed({ ok: false, tag: e._tag }),
141 ),
142 );
143 return yield* HttpServerResponse.json({ added, removed });
144 }
145
146 if (request.method === "POST" && pathname === "/channel-roles/bogus") {
147 const channelId = url.searchParams.get("channelId") ?? "";
148 const added = yield* addChannelRole({
149 channelId,
150 accessorIds: [BOGUS_ACCESSOR_ID],
151 channelRole: "EXPERT",
152 }).pipe(
153 Effect.map((r) => ({ ok: true, errors: r.errors.length })),
154 Effect.catchTag(
155 ["ValidationException", "ResourceNotFoundException"],
156 (e) => Effect.succeed({ ok: false, tag: e._tag }),
157 ),
158 );
159 const removed = yield* removeChannelRole({
160 channelId,
161 accessorIds: [BOGUS_ACCESSOR_ID],
162 channelRole: "EXPERT",
163 }).pipe(
164 Effect.map((r) => ({ ok: true, errors: r.errors.length })),
165 Effect.catchTag(
166 ["ValidationException", "ResourceNotFoundException"],
167 (e) => Effect.succeed({ ok: false, tag: e._tag }),
168 ),
169 );
170 return yield* HttpServerResponse.json({ added, removed });
171 }
172
173 if (request.method === "POST" && pathname === "/admins/bogus") {
174 // A nonexistent accessor id surfaces the typed 400/404 union —
175 // an IAM gap would surface AccessDeniedException instead, so the
176 // typed tag proves the grant reached the space-scoped API.
177 const register = yield* registerAdmin({
178 adminId: BOGUS_ACCESSOR_ID,
179 }).pipe(
180 Effect.map(() => ({ ok: true as const })),
181 Effect.catchTag(
182 ["ValidationException", "ResourceNotFoundException"],
183 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
184 ),
185 );
186 const deregister = yield* deregisterAdmin({
187 adminId: BOGUS_ACCESSOR_ID,
188 }).pipe(
189 Effect.map(() => ({ ok: true as const })),
190 Effect.catchTag(
191 ["ValidationException", "ResourceNotFoundException"],
192 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
193 ),
194 );
195 return yield* HttpServerResponse.json({ register, deregister });
196 }
197
198 if (request.method === "POST" && pathname === "/invites/bogus") {
199 const result = yield* sendInvites({
200 accessorIds: [BOGUS_ACCESSOR_ID],
201 title: "alchemy bindings probe",
202 body: "This invite targets a nonexistent accessor on purpose.",
203 }).pipe(
204 Effect.map(() => ({ ok: true as const })),
205 Effect.catchTag(
206 ["ValidationException", "ResourceNotFoundException"],
207 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
208 ),
209 );
210 return yield* HttpServerResponse.json(result);
211 }
212
213 return yield* HttpServerResponse.json(
214 { error: "Not found", method: request.method, pathname },
215 { status: 404 },
216 );
217 }).pipe(Effect.orDie),
218 };
219 }).pipe(
220 Effect.provide(
221 Layer.mergeAll(
222 RePostSpace.SendInvitesHttp,
223 RePostSpace.RegisterAdminHttp,
224 RePostSpace.DeregisterAdminHttp,
225 RePostSpace.BatchAddRoleHttp,
226 RePostSpace.BatchRemoveRoleHttp,
227 RePostSpace.CreateChannelHttp,
228 RePostSpace.GetChannelHttp,
229 RePostSpace.ListChannelsHttp,
230 RePostSpace.UpdateChannelHttp,
231 RePostSpace.BatchAddChannelRoleToAccessorsHttp,
232 RePostSpace.BatchRemoveChannelRoleFromAccessorsHttp,
233 ),
234 ),
235 ),
236);