1import * as Cognito from "@/AWS/Cognito";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Redacted from "effect/Redacted";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "trigger-handler.ts");
12
13const plain = (
14 value: string | Redacted.Redacted<string> | undefined,
15): string | undefined =>
16 value === undefined
17 ? undefined
18 : typeof value === "string"
19 ? value
20 : Redacted.value(value);
21
22const PASSWORD = "Alchemy-Trigger-Passw0rd!";
23
24export class CognitoTriggerFunction extends Lambda.Function<Lambda.Function>()(
25 "CognitoTriggerFunction",
26) {}
27
28/**
29 * Fixture for the user pool trigger e2e test: ONE Lambda that both hosts
30 * the pool's PreSignUp / PreTokenGeneration triggers (the circular case the
31 * binding-contract pattern exists for — the pool's LambdaConfig points back
32 * at this function) and drives the auth flows over its function URL.
33 */
34export default CognitoTriggerFunction.make(
35 {
36 main,
37 functionUrl: true,
38 timeout: Duration.seconds(30),
39 },
40 Effect.gen(function* () {
41 const pool = yield* Cognito.UserPool("TriggerUserPool", {
42 passwordPolicy: {
43 minimumLength: 12,
44 requireSymbols: false,
45 },
46 accountRecovery: [{ name: "admin_only", priority: 1 }],
47 tags: { Purpose: "cognito-trigger-fixture" },
48 });
49 const client = yield* Cognito.UserPoolClient("TriggerUserPoolClient", {
50 userPoolId: pool.userPoolId,
51 explicitAuthFlows: [
52 "ALLOW_USER_PASSWORD_AUTH",
53 "ALLOW_REFRESH_TOKEN_AUTH",
54 ],
55 });
56
57 // PreSignUp: auto-confirm every sign-up (and mark the email verified)
58 // so users arrive CONFIRMED without ever receiving a confirmation code.
59 yield* Cognito.onPreSignUp(pool, (event) =>
60 Effect.sync(() => Cognito.autoConfirmUser(event, { verifyEmail: true })),
61 );
62
63 // PreTokenGeneration: stamp a custom claim into every issued ID token.
64 yield* Cognito.onPreTokenGeneration(pool, (event) =>
65 Effect.sync(() => {
66 event.response.claimsOverrideDetails = {
67 claimsToAddOrOverride: { alchemyTrigger: "pre-token-generation" },
68 };
69 return event;
70 }),
71 );
72
73 const auth = yield* Cognito.UserPoolAuth(client);
74 const admin = yield* Cognito.UserPoolAdmin(pool);
75 const UserPoolId = yield* pool.userPoolId;
76 const ClientId = yield* client.clientId;
77
78 return {
79 fetch: Effect.gen(function* () {
80 const request = yield* HttpServerRequest;
81 const url = new URL(request.originalUrl);
82 const pathname = url.pathname;
83 const username = url.searchParams.get("username") ?? "missing-user";
84
85 // Sign up (PreSignUp auto-confirms) → adminGetUser (assert state)
86 // → password auth (PreTokenGeneration stamps a claim) → delete.
87 if (request.method === "POST" && pathname === "/sign-up-flow") {
88 // rerun tolerance: an earlier partial run may have left the user
89 yield* admin
90 .adminDeleteUser({ Username: username })
91 .pipe(Effect.catchTag("UserNotFoundException", () => Effect.void));
92
93 const signedUp = yield* auth.signUp({
94 Username: username,
95 Password: PASSWORD,
96 UserAttributes: [
97 { Name: "email", Value: `${username}@example.com` },
98 ],
99 });
100
101 const user = yield* admin.adminGetUser({ Username: username });
102 const emailVerified = plain(
103 (user.UserAttributes ?? []).find(
104 (attribute) => attribute.Name === "email_verified",
105 )?.Value,
106 );
107
108 const signIn = yield* auth.initiateAuth({
109 AuthFlow: "USER_PASSWORD_AUTH",
110 AuthParameters: { USERNAME: username, PASSWORD },
111 });
112
113 yield* admin.adminDeleteUser({ Username: username });
114
115 return yield* HttpServerResponse.json({
116 userConfirmed: signedUp.UserConfirmed,
117 userStatus: user.UserStatus,
118 emailVerified,
119 idToken: plain(signIn.AuthenticationResult?.IdToken),
120 });
121 }
122
123 if (request.method === "GET" && pathname === "/config") {
124 return yield* HttpServerResponse.json({
125 ok: true,
126 userPoolId: yield* UserPoolId,
127 clientId: yield* ClientId,
128 });
129 }
130
131 return yield* HttpServerResponse.json(
132 { error: "Not found", method: request.method, pathname },
133 { status: 404 },
134 );
135 }).pipe(Effect.orDie),
136 };
137 }).pipe(
138 Effect.provide(
139 Layer.mergeAll(
140 Lambda.UserPoolTriggerEventSource,
141 Cognito.UserPoolAuthHttp,
142 Cognito.UserPoolAdminHttp,
143 ),
144 ),
145 ),
146);