1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Namespace from "../../Namespace.ts";
4import { AWSEnvironment } from "../Environment.ts";
5import {
6 RoomMessageReviewEventSource as IVSChatRoomMessageReviewEventSource,
7 type RoomMessageEvent,
8 type RoomMessageReviewEventSourceService,
9 type RoomMessageReviewHandlerFn,
10 type RoomMessageReviewProps,
11} from "../IVSChat/RoomMessageReviewEventSource.ts";
12import type { Room } from "../IVSChat/Room.ts";
13import * as Lambda from "./Function.ts";
14import { Permission as LambdaPermission } from "./Permission.ts";
15
16/**
17 * An IVS Chat message review invocation — the PascalCase
18 * `Content`/`MessageId`/`RoomArn` envelope IVS Chat sends the handler for
19 * every `SendMessage` request.
20 */
21export const isRoomMessageEvent = (event: any): event is RoomMessageEvent =>
22 typeof event?.Content === "string" &&
23 typeof event?.MessageId === "string" &&
24 typeof event?.RoomArn === "string";
25
26/**
27 * Connects an IVS Chat room's message review handler to the current Lambda
28 * function.
29 *
30 * At deploy time this layer injects the function ARN into the room's
31 * `messageReviewHandler` through the room's binding contract and
32 * materializes the `lambda:InvokeFunction` Permission for
33 * `ivschat.amazonaws.com`; at runtime it dispatches review invocations
34 * (matched on `RoomArn`) to the registered handler and returns the verdict
35 * to IVS Chat.
36 * ### Reviewing room messages
37 * **Example:** Deny messages containing a banned word
38 * ```typescript
39 * yield* IVSChat.onReviewMessage(room, (event) =>
40 * Effect.succeed(
41 * event.Content.includes("banned-word")
42 * ? { ReviewResult: "DENY", Attributes: { Reason: "moderated" } }
43 * : undefined,
44 * ),
45 * );
46 * ```
47 *
48 * @binding
49 */
50export const RoomMessageReviewEventSource = Layer.effect(
51 IVSChatRoomMessageReviewEventSource,
52 Effect.gen(function* () {
53 // this layer can only be used in a Lambda Function
54 const host = yield* Lambda.Function;
55 const Permission = yield* LambdaPermission;
56
57 return Effect.fn(function* <Req = never>(
58 room: Room,
59 handler: RoomMessageReviewHandlerFn<Req>,
60 props?: RoomMessageReviewProps,
61 ) {
62 const RoomArn = yield* room.roomArn;
63
64 // Deploy-time: inject this function's ARN into the room's
65 // messageReviewHandler (the room's provider syncs it) and create the
66 // invoke Permission for ivschat. Skipped once running inside the
67 // deployed Function (the global guard), where the only work is
68 // registering the runtime dispatcher below. Namespaced under the
69 // host for stable logical identity.
70 if (!globalThis.__ALCHEMY_RUNTIME__) {
71 yield* Namespace.push(
72 host.LogicalId,
73 Effect.gen(function* () {
74 // IVS Chat VALIDATES the invoke permission when the handler is
75 // associated (CreateRoom/UpdateRoom fails with "invalid lambda
76 // permission" otherwise), so the Permission must exist BEFORE
77 // the room and cannot reference the room's ARN (that would be
78 // circular). Scope it to this account's rooms instead — the
79 // pattern the IVS Chat resource-policy docs recommend.
80 const { accountId, region } =
81 yield* AWSEnvironment.current as unknown as Effect.Effect<{
82 accountId: string;
83 region: string;
84 }>;
85 const permission = yield* Permission(
86 `${room.LogicalId}-MessageReview-Permission`,
87 {
88 action: "lambda:InvokeFunction",
89 functionName: host.functionArn,
90 principal: "ivschat.amazonaws.com",
91 sourceAccount: accountId,
92 sourceArn: `arn:aws:ivschat:${region}:${accountId}:room/*`,
93 },
94 );
95
96 // The Permission echoes the `functionName` prop (the function
97 // ARN) as an attribute — threading it as the handler `uri`
98 // makes the room reconcile only AFTER the invoke permission
99 // exists.
100 yield* room.bind`AWS.IVSChat.RoomMessageReview(${host}, ${room})`({
101 messageReviewHandler: {
102 uri: permission.functionName,
103 fallbackResult: props?.fallbackResult,
104 },
105 });
106 }),
107 );
108 }
109
110 yield* host.listen(
111 Effect.gen(function* () {
112 const roomArn = yield* RoomArn;
113
114 return (event: any) => {
115 if (isRoomMessageEvent(event) && event.RoomArn === roomArn) {
116 // Message review is request-response: the Lambda's return
117 // value IS the verdict IVS Chat applies. `void` from the
118 // handler allows the message unchanged.
119 return handler(event).pipe(
120 Effect.map(
121 (result) =>
122 result ?? {
123 ReviewResult: "ALLOW" as const,
124 Content: event.Content,
125 Attributes: event.Attributes,
126 },
127 ),
128 Effect.orDie,
129 );
130 }
131 };
132 }),
133 );
134 }) as RoomMessageReviewEventSourceService;
135 }),
136);