1import * as Lambda from "@/AWS/Lambda";
2import * as Rekognition from "@/AWS/Rekognition";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12// Deterministic collection id — this suite owns it in the testing account and
13// the lifecycle route deletes it on entry and exit, so reruns self-heal.
14export const TEST_COLLECTION_ID = "alchemy-test-rekognition-bindings";
15
16// A 100x100 RGB PNG (blue sky, green ground, yellow sun disc) generated once
17// and checked in — big enough for Rekognition's 80px minimum dimension. It
18// contains no faces, which the face routes exploit: IndexFaces succeeds with
19// zero face records and SearchFacesByImage surfaces the typed
20// InvalidParameterException ("no faces in image"), both proving IAM + call
21// plumbing end-to-end.
22const TEST_IMAGE_B64 =
23 "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAABNElEQVR4nO3YUQ3CMABF0cpBBN/IQc5EoGkOcMAXCkjK2u6l60n6v+Tsvo5Qnq+3UylQSNULwII15m5RFixlxb9FZghLWWaYJ3BnwVJWnGC5GX72268Dq4qpO1lZQaqXV1lEqotXWUeq3assJdXoBevSWI1ZtcQFC5ayzNCd5YKPfwfNEJayLrPEw8+d70cprPO8Wl7SlGUd9mp84sRY/ikd5dXl3cxdVg1Zx6dcBOucAwuWsswwT+DOgqWsOIEZwlJWnMAMYSkrTmCGsJQVJzBDWMqKE5ghLGXNNMP79nAqBWD90QosWGPuFmXBUlb8q22GsJRlhnkCdxYsZcUJzBCWsuIEZghLWXECM4SlrDiBGcJSVpzADGEpK05ghrCUFScwQ1jKihOYISxlxQnMENYWLusLBGA4iOtydmQAAAAASUVORK5CYII=";
24const imageBytes = Buffer.from(TEST_IMAGE_B64, "base64");
25
26// Well-formed-but-nonexistent identifiers used to drive the typed not-found
27// paths. An IAM gap would surface AccessDeniedException (a 500 through the
28// handler's orDie), so a typed not-found/validation tag proves the grant
29// end-to-end.
30const BOGUS_JOB_ID = "0".repeat(64);
31const BOGUS_FACE_ID = "00000000-0000-0000-0000-000000000000";
32const BOGUS_SESSION_ID = "00000000-0000-0000-0000-000000000000";
33const BOGUS_BUCKET = "alchemy-nonexistent-rekognition-test-bucket";
34const BOGUS_VIDEO = {
35 S3Object: { Bucket: BOGUS_BUCKET, Name: "video.mp4" },
36};
37
38export class RekognitionTestFunction extends Lambda.Function<Lambda.Function>()(
39 "RekognitionTestFunction",
40) {}
41
42export default RekognitionTestFunction.make(
43 {
44 main,
45 functionUrl: true,
46 // The image-analysis route runs seven sequential inferences and the
47 // collection route ~15 sequential data-plane calls.
48 timeout: Duration.seconds(120),
49 // The bundled Rekognition schema graph is large — the 128 MB default
50 // leaves almost no headroom (observed Max Memory Used: 118 MB on the
51 // cheap routes alone).
52 memorySize: 512,
53 },
54 Effect.gen(function* () {
55 // --- image analysis ---
56 const compareFaces = yield* Rekognition.CompareFaces();
57 const detectFaces = yield* Rekognition.DetectFaces();
58 const detectLabels = yield* Rekognition.DetectLabels();
59 const detectModerationLabels = yield* Rekognition.DetectModerationLabels();
60 const detectProtectiveEquipment =
61 yield* Rekognition.DetectProtectiveEquipment();
62 const detectText = yield* Rekognition.DetectText();
63 const recognizeCelebrities = yield* Rekognition.RecognizeCelebrities();
64 const getCelebrityInfo = yield* Rekognition.GetCelebrityInfo();
65
66 // --- face collections ---
67 const createCollection = yield* Rekognition.CreateCollection();
68 const deleteCollection = yield* Rekognition.DeleteCollection();
69 const describeCollection = yield* Rekognition.DescribeCollection();
70 const listCollections = yield* Rekognition.ListCollections();
71 const indexFaces = yield* Rekognition.IndexFaces();
72 const listFaces = yield* Rekognition.ListFaces();
73 const deleteFaces = yield* Rekognition.DeleteFaces();
74 const searchFaces = yield* Rekognition.SearchFaces();
75 const searchFacesByImage = yield* Rekognition.SearchFacesByImage();
76
77 // --- user search ---
78 const createUser = yield* Rekognition.CreateUser();
79 const deleteUser = yield* Rekognition.DeleteUser();
80 const listUsers = yield* Rekognition.ListUsers();
81 const associateFaces = yield* Rekognition.AssociateFaces();
82 const disassociateFaces = yield* Rekognition.DisassociateFaces();
83 const searchUsers = yield* Rekognition.SearchUsers();
84 const searchUsersByImage = yield* Rekognition.SearchUsersByImage();
85
86 // --- face liveness ---
87 const createFaceLivenessSession =
88 yield* Rekognition.CreateFaceLivenessSession();
89 const getFaceLivenessSessionResults =
90 yield* Rekognition.GetFaceLivenessSessionResults();
91
92 // --- video analysis ---
93 const startCelebrityRecognition =
94 yield* Rekognition.StartCelebrityRecognition();
95 const getCelebrityRecognition =
96 yield* Rekognition.GetCelebrityRecognition();
97 const startContentModeration = yield* Rekognition.StartContentModeration();
98 const getContentModeration = yield* Rekognition.GetContentModeration();
99 const startFaceDetection = yield* Rekognition.StartFaceDetection();
100 const getFaceDetection = yield* Rekognition.GetFaceDetection();
101 const startFaceSearch = yield* Rekognition.StartFaceSearch();
102 const getFaceSearch = yield* Rekognition.GetFaceSearch();
103 const startLabelDetection = yield* Rekognition.StartLabelDetection();
104 const getLabelDetection = yield* Rekognition.GetLabelDetection();
105 const startPersonTracking = yield* Rekognition.StartPersonTracking();
106 const getPersonTracking = yield* Rekognition.GetPersonTracking();
107 const startSegmentDetection = yield* Rekognition.StartSegmentDetection();
108 const getSegmentDetection = yield* Rekognition.GetSegmentDetection();
109 const startTextDetection = yield* Rekognition.StartTextDetection();
110 const getTextDetection = yield* Rekognition.GetTextDetection();
111
112 // --- media analysis jobs ---
113 const startMediaAnalysisJob = yield* Rekognition.StartMediaAnalysisJob();
114 const getMediaAnalysisJob = yield* Rekognition.GetMediaAnalysisJob();
115 const listMediaAnalysisJobs = yield* Rekognition.ListMediaAnalysisJobs();
116
117 // --- stream processors ---
118 const startStreamProcessor = yield* Rekognition.StartStreamProcessor();
119 const stopStreamProcessor = yield* Rekognition.StopStreamProcessor();
120 const describeStreamProcessor =
121 yield* Rekognition.DescribeStreamProcessor();
122 const listStreamProcessors = yield* Rekognition.ListStreamProcessors();
123
124 // --- custom labels ---
125 const detectCustomLabels = yield* Rekognition.DetectCustomLabels();
126 const describeProjects = yield* Rekognition.DescribeProjects();
127 const describeProjectVersions =
128 yield* Rekognition.DescribeProjectVersions();
129 const startProjectVersion = yield* Rekognition.StartProjectVersion();
130 const stopProjectVersion = yield* Rekognition.StopProjectVersion();
131
132 return {
133 fetch: Effect.gen(function* () {
134 const request = yield* HttpServerRequest;
135 const url = new URL(request.originalUrl);
136 const pathname = url.pathname;
137
138 // Cheap readiness route — no Rekognition call.
139 if (request.method === "GET" && pathname === "/ping") {
140 return yield* HttpServerResponse.json({ ok: true });
141 }
142
143 // One route drives every synchronous image-analysis binding against
144 // the embedded test image (real inferences; the image has no faces,
145 // so CompareFaces surfaces its typed InvalidParameterException).
146 if (request.method === "GET" && pathname === "/analyze-image") {
147 const labels = yield* detectLabels({
148 Image: { Bytes: imageBytes },
149 MaxLabels: 10,
150 });
151 const faces = yield* detectFaces({ Image: { Bytes: imageBytes } });
152 const moderation = yield* detectModerationLabels({
153 Image: { Bytes: imageBytes },
154 });
155 const text = yield* detectText({ Image: { Bytes: imageBytes } });
156 const ppe = yield* detectProtectiveEquipment({
157 Image: { Bytes: imageBytes },
158 });
159 const celebrities = yield* recognizeCelebrities({
160 Image: { Bytes: imageBytes },
161 });
162 const compareTag = yield* compareFaces({
163 SourceImage: { Bytes: imageBytes },
164 TargetImage: { Bytes: imageBytes },
165 }).pipe(
166 Effect.map(() => "Success"),
167 Effect.catchTag("InvalidParameterException", (e) =>
168 Effect.succeed(e._tag),
169 ),
170 );
171 const celebrityInfoTag = yield* getCelebrityInfo({
172 Id: "0000000000",
173 }).pipe(
174 Effect.map(() => "Success"),
175 Effect.catchTag(
176 ["ResourceNotFoundException", "InvalidParameterException"],
177 (e) => Effect.succeed(e._tag),
178 ),
179 );
180 return yield* HttpServerResponse.json({
181 labelNames: (labels.Labels ?? []).map((l) => l.Name),
182 faceCount: (faces.FaceDetails ?? []).length,
183 moderationCount: (moderation.ModerationLabels ?? []).length,
184 textCount: (text.TextDetections ?? []).length,
185 ppePersons: (ppe.Persons ?? []).length,
186 celebrityCount: (celebrities.CelebrityFaces ?? []).length,
187 compareTag,
188 celebrityInfoTag,
189 });
190 }
191
192 // Full real lifecycle of the collection + user data plane: create a
193 // collection, describe/list it, index a (faceless) image, create and
194 // list a user, drive the face-id ops through their typed not-found
195 // paths, then tear everything down.
196 if (request.method === "POST" && pathname === "/collections") {
197 // Self-heal from a previous crashed run.
198 yield* deleteCollection({ CollectionId: TEST_COLLECTION_ID }).pipe(
199 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
200 );
201 const created = yield* createCollection({
202 CollectionId: TEST_COLLECTION_ID,
203 }).pipe(
204 Effect.map((r) => r.StatusCode ?? 200),
205 Effect.catchTag("ResourceAlreadyExistsException", () =>
206 Effect.succeed(200),
207 ),
208 );
209 const described = yield* describeCollection({
210 CollectionId: TEST_COLLECTION_ID,
211 });
212 const collections = yield* listCollections({ MaxResults: 100 });
213 const indexed = yield* indexFaces({
214 CollectionId: TEST_COLLECTION_ID,
215 Image: { Bytes: imageBytes },
216 });
217 const faces = yield* listFaces({
218 CollectionId: TEST_COLLECTION_ID,
219 });
220 yield* createUser({
221 CollectionId: TEST_COLLECTION_ID,
222 UserId: "test-user",
223 }).pipe(Effect.catchTag("ConflictException", () => Effect.void));
224 const users = yield* listUsers({
225 CollectionId: TEST_COLLECTION_ID,
226 });
227 const searchUsersTag = yield* searchUsers({
228 CollectionId: TEST_COLLECTION_ID,
229 UserId: "test-user",
230 }).pipe(
231 Effect.map(() => "Success"),
232 Effect.catchTag(
233 ["InvalidParameterException", "ResourceNotFoundException"],
234 (e) => Effect.succeed(e._tag),
235 ),
236 );
237 const associateTag = yield* associateFaces({
238 CollectionId: TEST_COLLECTION_ID,
239 UserId: "test-user",
240 FaceIds: [BOGUS_FACE_ID],
241 }).pipe(
242 Effect.map(() => "Success"),
243 Effect.catchTag(
244 ["InvalidParameterException", "ResourceNotFoundException"],
245 (e) => Effect.succeed(e._tag),
246 ),
247 );
248 const disassociateTag = yield* disassociateFaces({
249 CollectionId: TEST_COLLECTION_ID,
250 UserId: "test-user",
251 FaceIds: [BOGUS_FACE_ID],
252 }).pipe(
253 Effect.map(() => "Success"),
254 Effect.catchTag(
255 ["InvalidParameterException", "ResourceNotFoundException"],
256 (e) => Effect.succeed(e._tag),
257 ),
258 );
259 const searchFacesTag = yield* searchFaces({
260 CollectionId: TEST_COLLECTION_ID,
261 FaceId: BOGUS_FACE_ID,
262 }).pipe(
263 Effect.map(() => "Success"),
264 Effect.catchTag(
265 ["InvalidParameterException", "ResourceNotFoundException"],
266 (e) => Effect.succeed(e._tag),
267 ),
268 );
269 const searchFacesByImageTag = yield* searchFacesByImage({
270 CollectionId: TEST_COLLECTION_ID,
271 Image: { Bytes: imageBytes },
272 }).pipe(
273 Effect.map(() => "Success"),
274 Effect.catchTag("InvalidParameterException", (e) =>
275 Effect.succeed(e._tag),
276 ),
277 );
278 const searchUsersByImageTag = yield* searchUsersByImage({
279 CollectionId: TEST_COLLECTION_ID,
280 Image: { Bytes: imageBytes },
281 }).pipe(
282 Effect.map(() => "Success"),
283 Effect.catchTag("InvalidParameterException", (e) =>
284 Effect.succeed(e._tag),
285 ),
286 );
287 const deleteFacesTag = yield* deleteFaces({
288 CollectionId: TEST_COLLECTION_ID,
289 FaceIds: [BOGUS_FACE_ID],
290 }).pipe(
291 Effect.map(() => "Success"),
292 Effect.catchTag("InvalidParameterException", (e) =>
293 Effect.succeed(e._tag),
294 ),
295 );
296 yield* deleteUser({
297 CollectionId: TEST_COLLECTION_ID,
298 UserId: "test-user",
299 }).pipe(
300 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
301 );
302 yield* deleteCollection({ CollectionId: TEST_COLLECTION_ID });
303 return yield* HttpServerResponse.json({
304 createStatus: created,
305 faceCountAtCreate: described.FaceCount ?? 0,
306 listedCollection: (collections.CollectionIds ?? []).includes(
307 TEST_COLLECTION_ID,
308 ),
309 indexedFaceRecords: (indexed.FaceRecords ?? []).length,
310 listedFaces: (faces.Faces ?? []).length,
311 listedUsers: (users.Users ?? []).map((u) => u.UserId),
312 searchUsersTag,
313 associateTag,
314 disassociateTag,
315 searchFacesTag,
316 searchFacesByImageTag,
317 searchUsersByImageTag,
318 deleteFacesTag,
319 });
320 }
321
322 // Real Face Liveness lifecycle: create a session, read its CREATED
323 // status back, and drive the typed SessionNotFoundException path.
324 if (request.method === "POST" && pathname === "/liveness") {
325 const session = yield* createFaceLivenessSession();
326 const results = yield* getFaceLivenessSessionResults({
327 SessionId: session.SessionId,
328 });
329 const notFoundTag = yield* getFaceLivenessSessionResults({
330 SessionId: BOGUS_SESSION_ID,
331 }).pipe(
332 Effect.map(() => "Found"),
333 Effect.catchTag("SessionNotFoundException", (e) =>
334 Effect.succeed(e._tag),
335 ),
336 );
337 return yield* HttpServerResponse.json({
338 sessionId: session.SessionId,
339 status: results.Status,
340 notFoundTag,
341 });
342 }
343
344 // Drives all eight Start* video bindings through Rekognition's typed
345 // server-side S3 validation (nonexistent bucket) — real runtime calls
346 // proving IAM without paying for eight async video jobs.
347 if (request.method === "POST" && pathname === "/video/start-all") {
348 const tags = {
349 celebrityRecognition: yield* startCelebrityRecognition({
350 Video: BOGUS_VIDEO,
351 }).pipe(
352 Effect.map(() => "Started"),
353 Effect.catch((e) => Effect.succeed(e._tag)),
354 ),
355 contentModeration: yield* startContentModeration({
356 Video: BOGUS_VIDEO,
357 }).pipe(
358 Effect.map(() => "Started"),
359 Effect.catch((e) => Effect.succeed(e._tag)),
360 ),
361 faceDetection: yield* startFaceDetection({
362 Video: BOGUS_VIDEO,
363 }).pipe(
364 Effect.map(() => "Started"),
365 Effect.catch((e) => Effect.succeed(e._tag)),
366 ),
367 // FaceSearch also validates the collection — it may surface
368 // ResourceNotFoundException before the S3 check.
369 faceSearch: yield* startFaceSearch({
370 Video: BOGUS_VIDEO,
371 CollectionId: "alchemy-nonexistent-collection",
372 }).pipe(
373 Effect.map(() => "Started"),
374 Effect.catch((e) => Effect.succeed(e._tag)),
375 ),
376 labelDetection: yield* startLabelDetection({
377 Video: BOGUS_VIDEO,
378 }).pipe(
379 Effect.map(() => "Started"),
380 Effect.catch((e) => Effect.succeed(e._tag)),
381 ),
382 personTracking: yield* startPersonTracking({
383 Video: BOGUS_VIDEO,
384 }).pipe(
385 Effect.map(() => "Started"),
386 Effect.catch((e) => Effect.succeed(e._tag)),
387 ),
388 segmentDetection: yield* startSegmentDetection({
389 Video: BOGUS_VIDEO,
390 SegmentTypes: ["SHOT"],
391 }).pipe(
392 Effect.map(() => "Started"),
393 Effect.catch((e) => Effect.succeed(e._tag)),
394 ),
395 textDetection: yield* startTextDetection({
396 Video: BOGUS_VIDEO,
397 }).pipe(
398 Effect.map(() => "Started"),
399 Effect.catch((e) => Effect.succeed(e._tag)),
400 ),
401 };
402 return yield* HttpServerResponse.json(tags);
403 }
404
405 // Drives all eight Get* video bindings through their typed
406 // ResourceNotFoundException path with a well-formed bogus JobId.
407 if (request.method === "GET" && pathname === "/video/get-all") {
408 const probe = { JobId: BOGUS_JOB_ID };
409 const tags = {
410 celebrityRecognition: yield* getCelebrityRecognition(probe).pipe(
411 Effect.map(() => "Found"),
412 Effect.catchTag(
413 [
414 "ResourceNotFoundException",
415 "InvalidParameterException",
416 "AccessDeniedException",
417 ],
418 (e) => Effect.succeed(e._tag),
419 ),
420 ),
421 contentModeration: yield* getContentModeration(probe).pipe(
422 Effect.map(() => "Found"),
423 Effect.catchTag(
424 [
425 "ResourceNotFoundException",
426 "InvalidParameterException",
427 "AccessDeniedException",
428 ],
429 (e) => Effect.succeed(e._tag),
430 ),
431 ),
432 faceDetection: yield* getFaceDetection(probe).pipe(
433 Effect.map(() => "Found"),
434 Effect.catchTag(
435 [
436 "ResourceNotFoundException",
437 "InvalidParameterException",
438 "AccessDeniedException",
439 ],
440 (e) => Effect.succeed(e._tag),
441 ),
442 ),
443 faceSearch: yield* getFaceSearch(probe).pipe(
444 Effect.map(() => "Found"),
445 Effect.catchTag(
446 [
447 "ResourceNotFoundException",
448 "InvalidParameterException",
449 "AccessDeniedException",
450 ],
451 (e) => Effect.succeed(e._tag),
452 ),
453 ),
454 labelDetection: yield* getLabelDetection(probe).pipe(
455 Effect.map(() => "Found"),
456 Effect.catchTag(
457 [
458 "ResourceNotFoundException",
459 "InvalidParameterException",
460 "AccessDeniedException",
461 ],
462 (e) => Effect.succeed(e._tag),
463 ),
464 ),
465 personTracking: yield* getPersonTracking(probe).pipe(
466 Effect.map(() => "Found"),
467 Effect.catchTag(
468 [
469 "ResourceNotFoundException",
470 "InvalidParameterException",
471 "AccessDeniedException",
472 ],
473 (e) => Effect.succeed(e._tag),
474 ),
475 ),
476 segmentDetection: yield* getSegmentDetection(probe).pipe(
477 Effect.map(() => "Found"),
478 Effect.catchTag(
479 [
480 "ResourceNotFoundException",
481 "InvalidParameterException",
482 "AccessDeniedException",
483 ],
484 (e) => Effect.succeed(e._tag),
485 ),
486 ),
487 textDetection: yield* getTextDetection(probe).pipe(
488 Effect.map(() => "Found"),
489 Effect.catchTag(
490 [
491 "ResourceNotFoundException",
492 "InvalidParameterException",
493 "AccessDeniedException",
494 ],
495 (e) => Effect.succeed(e._tag),
496 ),
497 ),
498 };
499 return yield* HttpServerResponse.json(tags);
500 }
501
502 // Media analysis: list for real, start/get through typed error paths.
503 if (request.method === "POST" && pathname === "/media-analysis") {
504 const jobCount = (
505 (yield* listMediaAnalysisJobs({ MaxResults: 10 }))
506 .MediaAnalysisJobs ?? []
507 ).length;
508 const startTag = yield* startMediaAnalysisJob({
509 OperationsConfig: {
510 DetectModerationLabels: { MinConfidence: 60 },
511 },
512 Input: {
513 S3Object: { Bucket: BOGUS_BUCKET, Name: "manifest.jsonl" },
514 },
515 OutputConfig: { S3Bucket: BOGUS_BUCKET, S3KeyPrefix: "out/" },
516 }).pipe(
517 Effect.map(() => "Started"),
518 Effect.catchTag(
519 [
520 "InvalidS3ObjectException",
521 "InvalidManifestException",
522 "InvalidParameterException",
523 "ResourceNotFoundException",
524 ],
525 (e) => Effect.succeed(e._tag),
526 ),
527 );
528 const getTag = yield* getMediaAnalysisJob({
529 JobId: BOGUS_JOB_ID,
530 }).pipe(
531 Effect.map(() => "Found"),
532 Effect.catchTag(
533 ["ResourceNotFoundException", "InvalidParameterException"],
534 (e) => Effect.succeed(e._tag),
535 ),
536 );
537 return yield* HttpServerResponse.json({ jobCount, startTag, getTag });
538 }
539
540 // Stream processors: list for real, control plane through the typed
541 // ResourceNotFoundException path. Rekognition Video stream processors
542 // are allow-listed per account: an unentitled account gets the typed
543 // AccessDeniedException from every op (the list included), which
544 // still proves the binding reached the service.
545 if (request.method === "GET" && pathname === "/stream-processors") {
546 const listed = yield* listStreamProcessors({ MaxResults: 10 }).pipe(
547 Effect.map(
548 (r) => ({ count: (r.StreamProcessors ?? []).length }) as const,
549 ),
550 Effect.catchTag("AccessDeniedException", (e) =>
551 Effect.succeed({ count: -1, listTag: e._tag } as const),
552 ),
553 );
554 const describeTag = yield* describeStreamProcessor({
555 Name: "alchemy-nonexistent-processor",
556 }).pipe(
557 Effect.map(() => "Found"),
558 Effect.catchTag(
559 ["ResourceNotFoundException", "AccessDeniedException"],
560 (e) => Effect.succeed(e._tag),
561 ),
562 );
563 const startTag = yield* startStreamProcessor({
564 Name: "alchemy-nonexistent-processor",
565 }).pipe(
566 Effect.map(() => "Started"),
567 Effect.catchTag(
568 ["ResourceNotFoundException", "AccessDeniedException"],
569 (e) => Effect.succeed(e._tag),
570 ),
571 );
572 const stopTag = yield* stopStreamProcessor({
573 Name: "alchemy-nonexistent-processor",
574 }).pipe(
575 Effect.map(() => "Stopped"),
576 Effect.catchTag(
577 ["ResourceNotFoundException", "AccessDeniedException"],
578 (e) => Effect.succeed(e._tag),
579 ),
580 );
581 return yield* HttpServerResponse.json({
582 ...listed,
583 describeTag,
584 startTag,
585 stopTag,
586 });
587 }
588
589 // Custom Labels: DescribeProjects for real; the model-scoped ops run
590 // through their typed ResourceNotFoundException path with well-formed
591 // same-account ARNs (?account=… is passed by the test, parsed from the
592 // deployed function's ARN). Custom Labels is closed to new customers,
593 // so entitlement-gated accounts may surface the typed
594 // AccessDeniedException instead — the test accepts both.
595 if (request.method === "GET" && pathname === "/custom-labels") {
596 const account = url.searchParams.get("account") ?? "123456789012";
597 const region = yield* Effect.sync(
598 () => process.env.AWS_REGION ?? "us-east-1",
599 );
600 const projectArn = `arn:aws:rekognition:${region}:${account}:project/alchemy-nonexistent/1700000000000`;
601 const projectVersionArn = `arn:aws:rekognition:${region}:${account}:project/alchemy-nonexistent/version/alchemy-nonexistent/1700000000000`;
602 const projectCount = (
603 (yield* describeProjects({ MaxResults: 10 })).ProjectDescriptions ??
604 []
605 ).length;
606 const describeVersionsTag = yield* describeProjectVersions({
607 ProjectArn: projectArn,
608 }).pipe(
609 Effect.map(() => "Found"),
610 Effect.catchTag(
611 [
612 "ResourceNotFoundException",
613 "InvalidParameterException",
614 "AccessDeniedException",
615 ],
616 (e) => Effect.succeed(e._tag),
617 ),
618 );
619 const detectTag = yield* detectCustomLabels({
620 ProjectVersionArn: projectVersionArn,
621 Image: { Bytes: imageBytes },
622 }).pipe(
623 Effect.map(() => "Detected"),
624 Effect.catchTag(
625 [
626 "ResourceNotFoundException",
627 "InvalidParameterException",
628 "AccessDeniedException",
629 ],
630 (e) => Effect.succeed(e._tag),
631 ),
632 );
633 const startTag = yield* startProjectVersion({
634 ProjectVersionArn: projectVersionArn,
635 MinInferenceUnits: 1,
636 }).pipe(
637 Effect.map(() => "Started"),
638 Effect.catchTag(
639 [
640 "ResourceNotFoundException",
641 "InvalidParameterException",
642 "AccessDeniedException",
643 ],
644 (e) => Effect.succeed(e._tag),
645 ),
646 );
647 const stopTag = yield* stopProjectVersion({
648 ProjectVersionArn: projectVersionArn,
649 }).pipe(
650 Effect.map(() => "Stopped"),
651 Effect.catchTag(
652 [
653 "ResourceNotFoundException",
654 "InvalidParameterException",
655 "AccessDeniedException",
656 ],
657 (e) => Effect.succeed(e._tag),
658 ),
659 );
660 return yield* HttpServerResponse.json({
661 projectCount,
662 describeVersionsTag,
663 detectTag,
664 startTag,
665 stopTag,
666 });
667 }
668
669 return yield* HttpServerResponse.json(
670 { error: "Not found", method: request.method, pathname },
671 { status: 404 },
672 );
673 }).pipe(Effect.orDie),
674 };
675 }).pipe(
676 Effect.provide(
677 Layer.mergeAll(
678 Rekognition.CompareFacesHttp,
679 Rekognition.DetectFacesHttp,
680 Rekognition.DetectLabelsHttp,
681 Rekognition.DetectModerationLabelsHttp,
682 Rekognition.DetectProtectiveEquipmentHttp,
683 Rekognition.DetectTextHttp,
684 Rekognition.RecognizeCelebritiesHttp,
685 Rekognition.GetCelebrityInfoHttp,
686 Rekognition.CreateCollectionHttp,
687 Rekognition.DeleteCollectionHttp,
688 Rekognition.DescribeCollectionHttp,
689 Rekognition.ListCollectionsHttp,
690 Rekognition.IndexFacesHttp,
691 Rekognition.ListFacesHttp,
692 Rekognition.DeleteFacesHttp,
693 Rekognition.SearchFacesHttp,
694 Rekognition.SearchFacesByImageHttp,
695 Rekognition.CreateUserHttp,
696 Rekognition.DeleteUserHttp,
697 Rekognition.ListUsersHttp,
698 Rekognition.AssociateFacesHttp,
699 Rekognition.DisassociateFacesHttp,
700 Rekognition.SearchUsersHttp,
701 Rekognition.SearchUsersByImageHttp,
702 Rekognition.CreateFaceLivenessSessionHttp,
703 Rekognition.GetFaceLivenessSessionResultsHttp,
704 Rekognition.StartCelebrityRecognitionHttp,
705 Rekognition.GetCelebrityRecognitionHttp,
706 Rekognition.StartContentModerationHttp,
707 Rekognition.GetContentModerationHttp,
708 Rekognition.StartFaceDetectionHttp,
709 Rekognition.GetFaceDetectionHttp,
710 Rekognition.StartFaceSearchHttp,
711 Rekognition.GetFaceSearchHttp,
712 Rekognition.StartLabelDetectionHttp,
713 Rekognition.GetLabelDetectionHttp,
714 Rekognition.StartPersonTrackingHttp,
715 Rekognition.GetPersonTrackingHttp,
716 Rekognition.StartSegmentDetectionHttp,
717 Rekognition.GetSegmentDetectionHttp,
718 Rekognition.StartTextDetectionHttp,
719 Rekognition.GetTextDetectionHttp,
720 Rekognition.StartMediaAnalysisJobHttp,
721 Rekognition.GetMediaAnalysisJobHttp,
722 Rekognition.ListMediaAnalysisJobsHttp,
723 Rekognition.StartStreamProcessorHttp,
724 Rekognition.StopStreamProcessorHttp,
725 Rekognition.DescribeStreamProcessorHttp,
726 Rekognition.ListStreamProcessorsHttp,
727 Rekognition.DetectCustomLabelsHttp,
728 Rekognition.DescribeProjectsHttp,
729 Rekognition.DescribeProjectVersionsHttp,
730 Rekognition.StartProjectVersionHttp,
731 Rekognition.StopProjectVersionHttp,
732 ),
733 ),
734 ),
735);