1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Namespace from "../../Namespace.ts";
4import {
5 UserPoolTriggerEventSource as CognitoUserPoolTriggerEventSource,
6 type UserPoolTriggerEvent,
7 type UserPoolTriggerEventSourceService,
8 type UserPoolTriggerHandler,
9 type UserPoolTriggerProps,
10 userPoolTriggerSourcePrefixes,
11} from "../Cognito/UserPoolTriggerEventSource.ts";
12import type { UserPool, UserPoolTriggerName } from "../Cognito/UserPool.ts";
13import * as Lambda from "./Function.ts";
14import { Permission as LambdaPermission } from "./Permission.ts";
15
16/**
17 * The `version`/`triggerSource`/`userPoolId`/`request`/`response` envelope
18 * shared by every user pool Lambda trigger. Declared structurally —
19 * `@types/aws-lambda` keeps `BaseTriggerEvent` in an internal `_common`
20 * module that its package root does not re-export.
21 */
22export interface UserPoolTriggerEnvelope {
23 version: string;
24 triggerSource: string;
25 region: string;
26 userPoolId: string;
27 userName?: string;
28 request: Record<string, unknown>;
29 response: Record<string, unknown>;
30}
31
32/**
33 * A Cognito user pool trigger invocation — the
34 * `version`/`triggerSource`/`userPoolId`/`request`/`response` envelope
35 * shared by every user pool Lambda trigger.
36 */
37export const isUserPoolTriggerEvent = (
38 event: any,
39): event is UserPoolTriggerEnvelope =>
40 typeof event?.version === "string" &&
41 typeof event?.triggerSource === "string" &&
42 typeof event?.userPoolId === "string" &&
43 typeof event?.request === "object" &&
44 event?.request !== null &&
45 typeof event?.response === "object" &&
46 event?.response !== null;
47
48/**
49 * Connects a Cognito user pool Lambda trigger to the current Lambda
50 * function.
51 *
52 * At deploy time this layer injects the function ARN into the pool's
53 * `LambdaConfig` through the pool's binding contract and materializes the
54 * `lambda:InvokeFunction` Permission for `cognito-idp.amazonaws.com`; at
55 * runtime it dispatches matching trigger events (matched on `userPoolId` +
56 * `triggerSource` prefix) to the registered handler and returns the
57 * handler's (mutated) event to Cognito.
58 * ### Handling user pool triggers
59 * **Example:** Auto-confirm sign-ups
60 * ```typescript
61 * yield* Cognito.onPreSignUp(pool, (event) =>
62 * Effect.sync(() => Cognito.autoConfirmUser(event, { verifyEmail: true })),
63 * );
64 * ```
65 *
66 * @binding
67 */
68export const UserPoolTriggerEventSource = Layer.effect(
69 CognitoUserPoolTriggerEventSource,
70 Effect.gen(function* () {
71 // this layer can only be used in a Lambda Function
72 const host = yield* Lambda.Function;
73 const Permission = yield* LambdaPermission;
74
75 return Effect.fn(function* <T extends UserPoolTriggerName, Req = never>(
76 userPool: UserPool,
77 props: UserPoolTriggerProps<T>,
78 handler: UserPoolTriggerHandler<T, Req>,
79 ) {
80 const UserPoolId = yield* userPool.userPoolId;
81
82 // Deploy-time: inject this function's ARN into the pool's
83 // LambdaConfig (the pool's provider syncs it) and create the invoke
84 // Permission for cognito-idp. Skipped once running inside the
85 // deployed Function (the global guard), where the only work is
86 // registering the runtime dispatcher below. Namespaced under the
87 // host for stable logical identity.
88 if (!globalThis.__ALCHEMY_RUNTIME__) {
89 yield* Namespace.push(
90 host.LogicalId,
91 Effect.gen(function* () {
92 yield* userPool.bind`AWS.Cognito.UserPoolTrigger(${host}, ${userPool}, ${props.trigger})`(
93 {
94 lambdaConfig: { [props.trigger]: host.functionArn },
95 },
96 );
97
98 yield* Permission(
99 `${userPool.LogicalId}-${props.trigger}-Permission`,
100 {
101 action: "lambda:InvokeFunction",
102 functionName: host.functionName,
103 principal: "cognito-idp.amazonaws.com",
104 sourceArn: userPool.userPoolArn,
105 },
106 );
107 }),
108 );
109 }
110
111 yield* host.listen(
112 Effect.gen(function* () {
113 const userPoolId = yield* UserPoolId;
114 const prefix = userPoolTriggerSourcePrefixes[props.trigger];
115
116 return (event: any) => {
117 if (
118 isUserPoolTriggerEvent(event) &&
119 event.userPoolId === userPoolId &&
120 event.triggerSource.startsWith(prefix)
121 ) {
122 // Cognito triggers are request-response: the Lambda's return
123 // value (the event with a mutated `response`) IS the trigger
124 // response. `void` from the handler answers with the event
125 // as-is.
126 return handler(event as UserPoolTriggerEvent<T>).pipe(
127 Effect.map((result) => result ?? event),
128 Effect.orDie,
129 );
130 }
131 };
132 }),
133 );
134 }) as UserPoolTriggerEventSourceService;
135 }),
136);