1import * as Cognito from "@/AWS/Cognito";
2import * as IAM from "@/AWS/IAM";
3import * as Lambda from "@/AWS/Lambda";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Redacted from "effect/Redacted";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "handler.ts");
13
14const plain = (
15 value: string | Redacted.Redacted<string> | undefined,
16): string | undefined =>
17 value === undefined
18 ? undefined
19 : typeof value === "string"
20 ? value
21 : Redacted.value(value);
22
23const PASSWORD = "Alchemy-Test-Passw0rd!";
24const NEW_PASSWORD = "Alchemy-Test-Passw0rd!2";
25
26export class CognitoTestFunction extends Lambda.Function<Lambda.Function>()(
27 "CognitoTestFunction",
28) {}
29
30export default CognitoTestFunction.make(
31 {
32 main,
33 functionUrl: true,
34 timeout: Duration.seconds(30),
35 },
36 Effect.gen(function* () {
37 const pool = yield* Cognito.UserPool("BindingsUserPool", {
38 passwordPolicy: {
39 minimumLength: 12,
40 requireSymbols: false,
41 },
42 accountRecovery: [{ name: "admin_only", priority: 1 }],
43 tags: { Purpose: "cognito-bindings-fixture" },
44 });
45 const client = yield* Cognito.UserPoolClient("BindingsUserPoolClient", {
46 userPoolId: pool.userPoolId,
47 explicitAuthFlows: [
48 "ALLOW_USER_PASSWORD_AUTH",
49 "ALLOW_ADMIN_USER_PASSWORD_AUTH",
50 "ALLOW_REFRESH_TOKEN_AUTH",
51 ],
52 });
53 const group = yield* Cognito.Group("BindingsGroup", {
54 userPoolId: pool.userPoolId,
55 description: "cognito bindings fixture group",
56 });
57
58 const identities = yield* Cognito.IdentityPool("BindingsIdentityPool", {
59 allowUnauthenticatedIdentities: true,
60 allowClassicFlow: true,
61 tags: { Purpose: "cognito-bindings-fixture" },
62 });
63 const guestRole = yield* IAM.Role("BindingsGuestRole", {
64 assumeRolePolicyDocument: {
65 Version: "2012-10-17",
66 Statement: [
67 {
68 Effect: "Allow",
69 Principal: { Federated: "cognito-identity.amazonaws.com" },
70 Action: ["sts:AssumeRoleWithWebIdentity"],
71 Condition: {
72 StringEquals: {
73 "cognito-identity.amazonaws.com:aud": identities.identityPoolId,
74 },
75 "ForAnyValue:StringLike": {
76 "cognito-identity.amazonaws.com:amr": "unauthenticated",
77 },
78 },
79 },
80 ],
81 },
82 });
83 yield* Cognito.IdentityPoolRoleAttachment("BindingsIdentityRoles", {
84 identityPoolId: identities.identityPoolId,
85 roles: { unauthenticated: guestRole.roleArn },
86 });
87
88 const admin = yield* Cognito.UserPoolAdmin(pool);
89 const auth = yield* Cognito.UserPoolAuth(client);
90 const identityAuth = yield* Cognito.IdentityPoolAuth(identities);
91 const identityAdmin = yield* Cognito.IdentityPoolAdmin(identities);
92 // Output → deferred effect; resolved per-request inside the handler.
93 const GroupName = yield* group.groupName;
94 const UserPoolId = yield* pool.userPoolId;
95 const ClientId = yield* client.clientId;
96
97 return {
98 fetch: Effect.gen(function* () {
99 const request = yield* HttpServerRequest;
100 const url = new URL(request.originalUrl);
101 const pathname = url.pathname;
102 const username = url.searchParams.get("username") ?? "missing-user";
103
104 // Full password auth flow: create → set permanent password →
105 // USER_PASSWORD_AUTH → getUser with the access token → delete.
106 if (request.method === "POST" && pathname === "/auth-flow") {
107 yield* admin
108 .adminCreateUser({
109 Username: username,
110 MessageAction: "SUPPRESS",
111 UserAttributes: [
112 { Name: "email", Value: `${username}@example.com` },
113 { Name: "email_verified", Value: "true" },
114 ],
115 })
116 .pipe(
117 // rerun tolerance: an earlier partial run may have left the user
118 Effect.catchTag("UsernameExistsException", () => Effect.void),
119 );
120 yield* admin.adminSetUserPassword({
121 Username: username,
122 Password: PASSWORD,
123 Permanent: true,
124 });
125 const signIn = yield* auth.initiateAuth({
126 AuthFlow: "USER_PASSWORD_AUTH",
127 AuthParameters: { USERNAME: username, PASSWORD },
128 });
129 const accessToken = plain(signIn.AuthenticationResult?.AccessToken);
130 const me = accessToken
131 ? yield* auth.getUser({ AccessToken: accessToken })
132 : undefined;
133 yield* admin.adminDeleteUser({ Username: username });
134 return yield* HttpServerResponse.json({
135 challengeName: signIn.ChallengeName,
136 idToken: plain(signIn.AuthenticationResult?.IdToken),
137 accessToken,
138 refreshToken: plain(signIn.AuthenticationResult?.RefreshToken),
139 tokenType: signIn.AuthenticationResult?.TokenType,
140 getUserUsername: plain(me?.Username),
141 });
142 }
143
144 // Admin user lifecycle: create → get → disable → enable → group
145 // membership → list → delete.
146 if (request.method === "POST" && pathname === "/user-lifecycle") {
147 yield* admin
148 .adminCreateUser({
149 Username: username,
150 MessageAction: "SUPPRESS",
151 UserAttributes: [
152 { Name: "email", Value: `${username}@example.com` },
153 ],
154 })
155 .pipe(
156 Effect.catchTag("UsernameExistsException", () => Effect.void),
157 );
158 const created = yield* admin.adminGetUser({ Username: username });
159 yield* admin.adminDisableUser({ Username: username });
160 const disabled = yield* admin.adminGetUser({ Username: username });
161 yield* admin.adminEnableUser({ Username: username });
162 const groupName = yield* GroupName;
163 yield* admin.adminAddUserToGroup({
164 Username: username,
165 GroupName: groupName,
166 });
167 const inGroup = yield* admin.listUsersInGroup({
168 GroupName: groupName,
169 });
170 yield* admin.adminRemoveUserFromGroup({
171 Username: username,
172 GroupName: groupName,
173 });
174 const listed = yield* admin.listUsers({
175 Filter: `username = "${username}"`,
176 });
177 yield* admin.adminDeleteUser({ Username: username });
178 const gone = yield* admin.adminGetUser({ Username: username }).pipe(
179 Effect.map(() => false),
180 Effect.catchTag("UserNotFoundException", () =>
181 Effect.succeed(true),
182 ),
183 );
184 return yield* HttpServerResponse.json({
185 createdStatus: created.UserStatus,
186 disabledEnabled: disabled.Enabled,
187 groupMembers: (inGroup.Users ?? []).map((u) => plain(u.Username)),
188 listedCount: (listed.Users ?? []).length,
189 deleted: gone,
190 });
191 }
192
193 // Public sign-up (admin-confirmed since we cannot receive the email
194 // code) followed by password auth.
195 if (request.method === "POST" && pathname === "/sign-up-flow") {
196 const signedUp = yield* auth
197 .signUp({
198 Username: username,
199 Password: PASSWORD,
200 UserAttributes: [
201 { Name: "email", Value: `${username}@example.com` },
202 ],
203 })
204 .pipe(
205 Effect.map((r) => ({
206 userConfirmed: r.UserConfirmed,
207 userSub: r.UserSub,
208 })),
209 Effect.catchTag("UsernameExistsException", () =>
210 Effect.succeed({
211 userConfirmed: false,
212 userSub: "already-exists",
213 }),
214 ),
215 );
216 yield* admin.adminConfirmSignUp({ Username: username }).pipe(
217 // NotAuthorizedException: already CONFIRMED on rerun
218 Effect.catchTag("NotAuthorizedException", () => Effect.void),
219 );
220 const confirmed = yield* admin.adminGetUser({ Username: username });
221 const signIn = yield* auth.initiateAuth({
222 AuthFlow: "USER_PASSWORD_AUTH",
223 AuthParameters: { USERNAME: username, PASSWORD },
224 });
225 yield* admin.adminDeleteUser({ Username: username });
226 return yield* HttpServerResponse.json({
227 userConfirmed: signedUp.userConfirmed,
228 userSub: signedUp.userSub,
229 confirmedStatus: confirmed.UserStatus,
230 hasIdToken: signIn.AuthenticationResult?.IdToken !== undefined,
231 });
232 }
233
234 // Self-service flows: change password, update/delete own attributes,
235 // refresh-token exchange, self-deletion.
236 if (request.method === "POST" && pathname === "/self-service") {
237 yield* admin
238 .adminCreateUser({
239 Username: username,
240 MessageAction: "SUPPRESS",
241 UserAttributes: [
242 { Name: "email", Value: `${username}@example.com` },
243 { Name: "email_verified", Value: "true" },
244 ],
245 })
246 .pipe(
247 Effect.catchTag("UsernameExistsException", () => Effect.void),
248 );
249 yield* admin.adminSetUserPassword({
250 Username: username,
251 Password: PASSWORD,
252 Permanent: true,
253 });
254 const signIn = yield* auth.initiateAuth({
255 AuthFlow: "USER_PASSWORD_AUTH",
256 AuthParameters: { USERNAME: username, PASSWORD },
257 });
258 const firstToken = plain(signIn.AuthenticationResult?.AccessToken)!;
259 yield* auth.changePassword({
260 AccessToken: firstToken,
261 PreviousPassword: PASSWORD,
262 ProposedPassword: NEW_PASSWORD,
263 });
264 const reAuth = yield* auth.initiateAuth({
265 AuthFlow: "USER_PASSWORD_AUTH",
266 AuthParameters: { USERNAME: username, PASSWORD: NEW_PASSWORD },
267 });
268 const accessToken = plain(reAuth.AuthenticationResult?.AccessToken)!;
269 yield* auth.updateUserAttributes({
270 AccessToken: accessToken,
271 UserAttributes: [{ Name: "nickname", Value: "self-service" }],
272 });
273 const me = yield* auth.getUser({ AccessToken: accessToken });
274 const nickname = plain(
275 me.UserAttributes?.find((a) => a.Name === "nickname")?.Value,
276 );
277 yield* auth.deleteUserAttributes({
278 AccessToken: accessToken,
279 UserAttributeNames: ["nickname"],
280 });
281 const meAfter = yield* auth.getUser({ AccessToken: accessToken });
282 const nicknameAfter = plain(
283 meAfter.UserAttributes?.find((a) => a.Name === "nickname")?.Value,
284 );
285 const refreshed = yield* auth.getTokensFromRefreshToken({
286 RefreshToken: plain(reAuth.AuthenticationResult?.RefreshToken)!,
287 });
288 yield* auth.deleteUser({ AccessToken: accessToken });
289 const gone = yield* admin.adminGetUser({ Username: username }).pipe(
290 Effect.map(() => false),
291 Effect.catchTag("UserNotFoundException", () =>
292 Effect.succeed(true),
293 ),
294 );
295 return yield* HttpServerResponse.json({
296 changedPasswordAuth:
297 reAuth.AuthenticationResult?.AccessToken !== undefined,
298 nickname,
299 nicknameAfter: nicknameAfter ?? null,
300 refreshedHasAccessToken:
301 refreshed.AuthenticationResult?.AccessToken !== undefined,
302 deleted: gone,
303 });
304 }
305
306 // Extended admin surface: attribute deletion, group listings,
307 // device listing.
308 if (request.method === "POST" && pathname === "/admin-extended") {
309 yield* admin
310 .adminCreateUser({
311 Username: username,
312 MessageAction: "SUPPRESS",
313 UserAttributes: [
314 { Name: "email", Value: `${username}@example.com` },
315 ],
316 })
317 .pipe(
318 Effect.catchTag("UsernameExistsException", () => Effect.void),
319 );
320 yield* admin.adminUpdateUserAttributes({
321 Username: username,
322 UserAttributes: [{ Name: "nickname", Value: "admin-extended" }],
323 });
324 const withNickname = yield* admin.adminGetUser({
325 Username: username,
326 });
327 yield* admin.adminDeleteUserAttributes({
328 Username: username,
329 UserAttributeNames: ["nickname"],
330 });
331 const withoutNickname = yield* admin.adminGetUser({
332 Username: username,
333 });
334 const groupName = yield* GroupName;
335 yield* admin.adminAddUserToGroup({
336 Username: username,
337 GroupName: groupName,
338 });
339 const userGroups = yield* admin.adminListGroupsForUser({
340 Username: username,
341 });
342 const allGroups = yield* admin.listGroups();
343 const deviceCount = yield* admin
344 .adminListDevices({ Username: username })
345 .pipe(
346 Effect.map((r) => (r.Devices ?? []).length),
347 // device tracking is not configured on the fixture pool
348 Effect.catchTag("InvalidUserPoolConfigurationException", () =>
349 Effect.succeed(-1),
350 ),
351 );
352 yield* admin.adminDeleteUser({ Username: username });
353 const nicknameOf = (user: typeof withNickname): string | undefined =>
354 plain(
355 user.UserAttributes?.find((a) => a.Name === "nickname")?.Value,
356 );
357 return yield* HttpServerResponse.json({
358 nickname: nicknameOf(withNickname),
359 nicknameAfter: nicknameOf(withoutNickname) ?? null,
360 userGroups: (userGroups.Groups ?? []).map((g) => g.GroupName),
361 allGroups: (allGroups.Groups ?? []).map((g) => g.GroupName),
362 deviceCount,
363 });
364 }
365
366 // Identity pool data plane: guest identity → AWS credentials and
367 // OIDC token, then identity administration.
368 if (request.method === "POST" && pathname === "/identity-flow") {
369 const got = yield* identityAuth.getId();
370 const identityId = got.IdentityId!;
371 const creds = yield* identityAuth.getCredentialsForIdentity({
372 IdentityId: identityId,
373 });
374 const openId = yield* identityAuth.getOpenIdToken({
375 IdentityId: identityId,
376 });
377 const described = yield* identityAdmin.describeIdentity({
378 IdentityId: identityId,
379 });
380 const listed = yield* identityAdmin.listIdentities({
381 MaxResults: 60,
382 });
383 yield* identityAdmin.deleteIdentities({
384 IdentityIdsToDelete: [identityId],
385 });
386 return yield* HttpServerResponse.json({
387 identityId,
388 hasAccessKeyId: creds.Credentials?.AccessKeyId !== undefined,
389 hasSessionToken: creds.Credentials?.SessionToken !== undefined,
390 hasOpenIdToken: openId.Token !== undefined,
391 describedIdentityId: described.IdentityId,
392 listedContains: (listed.Identities ?? []).some(
393 (identity) => identity.IdentityId === identityId,
394 ),
395 });
396 }
397
398 if (request.method === "GET" && pathname === "/config") {
399 return yield* HttpServerResponse.json({
400 ok: true,
401 userPoolId: yield* UserPoolId,
402 clientId: yield* ClientId,
403 });
404 }
405
406 return yield* HttpServerResponse.json(
407 { error: "Not found", method: request.method, pathname },
408 { status: 404 },
409 );
410 }).pipe(
411 // Surface typed operation failures in the 500 body so the test's
412 // retry logging shows the real cause instead of a generic message.
413 Effect.catch((error) =>
414 HttpServerResponse.json({ error: String(error) }, { status: 500 }),
415 ),
416 Effect.orDie,
417 ),
418 };
419 }).pipe(
420 Effect.provide(
421 Layer.mergeAll(
422 Cognito.UserPoolAdminHttp,
423 Cognito.UserPoolAuthHttp,
424 Cognito.IdentityPoolAuthHttp,
425 Cognito.IdentityPoolAdminHttp,
426 ),
427 ),
428 ),
429);