1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Namespace from "../../Namespace.ts";
4import * as Lambda from "../Lambda/Function.ts";
5import { Permission as LambdaPermission } from "../Lambda/Permission.ts";
6import type { BotAlias } from "./BotAlias.ts";
7import {
8 CodeHookEventSource,
9 type CodeHookEvent,
10 type CodeHookEventSourceService,
11 type CodeHookHandler,
12 type CodeHookProps,
13} from "./CodeHookEventSource.ts";
14
15/**
16 * An Amazon Lex V2 code hook invocation — the
17 * `messageVersion`/`invocationSource`/`bot`/`sessionState` envelope Lex
18 * sends to dialog and fulfillment Lambda functions.
19 */
20export const isCodeHookEvent = (event: any): event is CodeHookEvent =>
21 typeof event?.messageVersion === "string" &&
22 (event?.invocationSource === "DialogCodeHook" ||
23 event?.invocationSource === "FulfillmentCodeHook") &&
24 typeof event?.bot === "object" &&
25 event?.bot !== null &&
26 typeof event?.bot?.id === "string" &&
27 typeof event?.bot?.aliasId === "string" &&
28 typeof event?.sessionState === "object" &&
29 event?.sessionState !== null;
30
31/**
32 * Connects an Amazon Lex V2 bot alias's Lambda code hook to the current
33 * Lambda function.
34 *
35 * At deploy time this layer injects the function ARN into the alias's
36 * `botAliasLocaleSettings` through the alias's binding contract and
37 * materializes the `lambda:InvokeFunction` Permission for
38 * `lexv2.amazonaws.com`; at runtime it dispatches matching code hook events
39 * (matched on the bot id, alias id, and locale) to the registered handler
40 * and returns the handler's response to Lex.
41 * ### Handling Code Hooks
42 * **Example:** Fulfill an intent
43 * ```typescript
44 * yield* LexV2.onCodeHook(alias, { localeId: "en_US" }, (event) =>
45 * Effect.succeed(LexV2.fulfillIntent(event, { message: "Done!" })),
46 * );
47 * ```
48 *
49 * @binding
50 */
51export const LambdaCodeHookEventSource = Layer.effect(
52 CodeHookEventSource,
53 Effect.gen(function* () {
54 // this layer can only be used in a Lambda Function
55 const host = yield* Lambda.Function;
56
57 return Effect.fn(function* <Req = never>(
58 alias: BotAlias,
59 props: CodeHookProps,
60 handler: CodeHookHandler<Req>,
61 ) {
62 const BotId = yield* alias.botId;
63 const BotAliasId = yield* alias.botAliasId;
64
65 // Deploy-time: inject this function's ARN into the alias's locale
66 // settings (the alias's provider syncs it) and create the invoke
67 // Permission for lexv2. Skipped once running inside the deployed
68 // Function (the global guard), where the only work is registering
69 // the runtime dispatcher below. Namespaced under the host for stable
70 // logical identity.
71 if (!globalThis.__ALCHEMY_RUNTIME__) {
72 yield* Namespace.push(
73 host.LogicalId,
74 Effect.gen(function* () {
75 yield* alias.bind`AWS.LexV2.CodeHook(${host}, ${alias}, ${props.localeId})`(
76 {
77 codeHooks: { [props.localeId]: host.functionArn },
78 },
79 );
80
81 yield* LambdaPermission(
82 `${alias.LogicalId}-${props.localeId}-CodeHookPermission`,
83 {
84 action: "lambda:InvokeFunction",
85 functionName: host.functionName,
86 principal: "lexv2.amazonaws.com",
87 sourceArn: alias.botAliasArn,
88 },
89 );
90 }),
91 );
92 }
93
94 yield* host.listen(
95 Effect.gen(function* () {
96 const botId = yield* BotId;
97 const botAliasId = yield* BotAliasId;
98
99 return (event: any) => {
100 if (
101 isCodeHookEvent(event) &&
102 event.bot.id === botId &&
103 event.bot.aliasId === botAliasId &&
104 event.bot.localeId === props.localeId
105 ) {
106 // Code hooks are request-response: the Lambda's return value
107 // (sessionState + messages) IS the hook response.
108 return handler(event).pipe(Effect.orDie);
109 }
110 };
111 }),
112 );
113 }) as CodeHookEventSourceService;
114 }),
115);