1import * as ECRPublic from "@/AWS/ECRPublic";
2import * as Lambda from "@/AWS/Lambda";
3import crypto from "node:crypto";
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
14// A well-formed-but-nonexistent layer digest — batchCheckLayerAvailability
15// reports it as a per-layer failure, proving the repository-scoped grant.
16const NONEXISTENT_DIGEST = `sha256:${"0".repeat(64)}`;
17
18const sha256Hex = (bytes: Uint8Array) =>
19 Effect.sync(() => crypto.createHash("sha256").update(bytes).digest("hex"));
20
21export class EcrPublicTestFunction extends Lambda.Function<Lambda.Function>()(
22 "EcrPublicTestFunction",
23) {}
24
25export default EcrPublicTestFunction.make(
26 {
27 main,
28 functionUrl: true,
29 // The push-flow route performs several sequential ECR Public calls; the
30 // AWS default 3s Lambda timeout is too tight.
31 timeout: Duration.seconds(60),
32 },
33 Effect.gen(function* () {
34 const repository = yield* ECRPublic.PublicRepository("BindingsRepo", {
35 catalogData: {
36 description: "alchemy ECRPublic bindings fixture",
37 architectures: ["x86-64"],
38 operatingSystems: ["Linux"],
39 },
40 });
41
42 // --- repository-scoped bindings ---
43 const describeImages = yield* ECRPublic.DescribeImages(repository);
44 const describeImageTags = yield* ECRPublic.DescribeImageTags(repository);
45 const getRepositoryCatalogData =
46 yield* ECRPublic.GetRepositoryCatalogData(repository);
47 const batchCheckLayerAvailability =
48 yield* ECRPublic.BatchCheckLayerAvailability(repository);
49 const batchDeleteImage = yield* ECRPublic.BatchDeleteImage(repository);
50 const initiateLayerUpload =
51 yield* ECRPublic.InitiateLayerUpload(repository);
52 const uploadLayerPart = yield* ECRPublic.UploadLayerPart(repository);
53 const completeLayerUpload =
54 yield* ECRPublic.CompleteLayerUpload(repository);
55 const putImage = yield* ECRPublic.PutImage(repository);
56
57 // --- registry-level bindings ---
58 const getAuthorizationToken = yield* ECRPublic.GetAuthorizationToken();
59 const describeRegistries = yield* ECRPublic.DescribeRegistries();
60 const getRegistryCatalogData = yield* ECRPublic.GetRegistryCatalogData();
61
62 const bound = {
63 describeImages,
64 describeImageTags,
65 getRepositoryCatalogData,
66 batchCheckLayerAvailability,
67 batchDeleteImage,
68 initiateLayerUpload,
69 uploadLayerPart,
70 completeLayerUpload,
71 putImage,
72 getAuthorizationToken,
73 describeRegistries,
74 getRegistryCatalogData,
75 };
76
77 /**
78 * Upload one blob as a layer: initiate → upload the single (last) part →
79 * complete with the blob's sha256. A re-run finds the layer already
80 * sealed — `LayerAlreadyExistsException` converges to the same digest.
81 */
82 const uploadBlob = Effect.fn(function* (blob: Uint8Array) {
83 const digest = `sha256:${yield* sha256Hex(blob)}`;
84 const { uploadId } = yield* initiateLayerUpload();
85 yield* uploadLayerPart({
86 uploadId: uploadId!,
87 partFirstByte: 0,
88 partLastByte: blob.byteLength - 1,
89 layerPartBlob: blob,
90 });
91 yield* completeLayerUpload({
92 uploadId: uploadId!,
93 layerDigests: [digest],
94 }).pipe(
95 Effect.catchTag("LayerAlreadyExistsException", () => Effect.void),
96 );
97 return digest;
98 });
99
100 return {
101 fetch: Effect.gen(function* () {
102 const request = yield* HttpServerRequest;
103 const url = new URL(request.originalUrl);
104 const pathname = url.pathname;
105
106 if (request.method === "GET" && pathname === "/bindings") {
107 return yield* HttpServerResponse.json({
108 bound: Object.keys(bound),
109 });
110 }
111
112 if (request.method === "GET" && pathname === "/registries") {
113 const result = yield* describeRegistries();
114 const registry = result.registries?.[0];
115 return yield* HttpServerResponse.json({
116 count: (result.registries ?? []).length,
117 alias: registry?.aliases?.[0]?.name ?? null,
118 });
119 }
120
121 if (request.method === "GET" && pathname === "/registry-catalog") {
122 const result = yield* getRegistryCatalogData();
123 return yield* HttpServerResponse.json({
124 tag: "Ok",
125 displayName: result.registryCatalogData.displayName ?? null,
126 });
127 }
128
129 if (request.method === "GET" && pathname === "/auth-token") {
130 const result = yield* getAuthorizationToken();
131 const token = result.authorizationData?.authorizationToken;
132 // The token is Redacted in the distilled response — never echo it.
133 const hasToken =
134 token !== undefined &&
135 (typeof token === "string"
136 ? token.length > 0
137 : Redacted.value(token).length > 0);
138 return yield* HttpServerResponse.json({ hasToken });
139 }
140
141 if (request.method === "GET" && pathname === "/repo-catalog") {
142 const result = yield* getRepositoryCatalogData();
143 return yield* HttpServerResponse.json({
144 description: result.catalogData?.description ?? null,
145 });
146 }
147
148 if (request.method === "GET" && pathname === "/images") {
149 const result = yield* describeImages();
150 return yield* HttpServerResponse.json({
151 count: (result.imageDetails ?? []).length,
152 });
153 }
154
155 if (request.method === "GET" && pathname === "/image-tags") {
156 const result = yield* describeImageTags();
157 return yield* HttpServerResponse.json({
158 count: (result.imageTagDetails ?? []).length,
159 });
160 }
161
162 if (request.method === "GET" && pathname === "/check-layers") {
163 const result = yield* batchCheckLayerAvailability({
164 layerDigests: [NONEXISTENT_DIGEST],
165 });
166 // ECR Public reports a nonexistent digest either as a per-layer
167 // failure or as a layer with `layerAvailability: "UNAVAILABLE"`.
168 return yield* HttpServerResponse.json({
169 failures: (result.failures ?? []).length,
170 unavailable: (result.layers ?? []).filter(
171 (l) => l.layerAvailability !== "AVAILABLE",
172 ).length,
173 });
174 }
175
176 if (request.method === "GET" && pathname === "/delete-missing") {
177 const result = yield* batchDeleteImage({
178 imageIds: [{ imageTag: "does-not-exist" }],
179 });
180 return yield* HttpServerResponse.json({
181 failureCode: result.failures?.[0]?.failureCode ?? null,
182 });
183 }
184
185 if (request.method === "GET" && pathname === "/push-flow") {
186 // A genuine (minimal) image push: upload a config blob and a layer
187 // blob, then put a docker v2 manifest referencing both. Re-runs
188 // converge — existing layers and an identical manifest are fine.
189 const layerBlob = new TextEncoder().encode(
190 "alchemy-ecr-public-bindings-fixture-layer",
191 );
192 const configBlob = new TextEncoder().encode(
193 JSON.stringify({
194 architecture: "amd64",
195 os: "linux",
196 config: {},
197 rootfs: { type: "layers", diff_ids: [NONEXISTENT_DIGEST] },
198 }),
199 );
200 const layerDigest = yield* uploadBlob(layerBlob);
201 const configDigest = yield* uploadBlob(configBlob);
202 const imageManifest = JSON.stringify({
203 schemaVersion: 2,
204 mediaType: "application/vnd.docker.distribution.manifest.v2+json",
205 config: {
206 mediaType: "application/vnd.docker.container.image.v1+json",
207 size: configBlob.byteLength,
208 digest: configDigest,
209 },
210 layers: [
211 {
212 mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
213 size: layerBlob.byteLength,
214 digest: layerDigest,
215 },
216 ],
217 });
218 const result = yield* putImage({
219 imageManifest,
220 imageManifestMediaType:
221 "application/vnd.docker.distribution.manifest.v2+json",
222 imageTag: "bindings-test",
223 }).pipe(
224 Effect.map((r) => ({
225 tag: "Ok",
226 imageDigest: r.image?.imageId?.imageDigest ?? null,
227 })),
228 // Re-running the route pushes the identical manifest+tag again.
229 Effect.catchTag(
230 ["ImageAlreadyExistsException", "ImageTagAlreadyExistsException"],
231 (e) => Effect.succeed({ tag: e._tag, imageDigest: null }),
232 ),
233 );
234 return yield* HttpServerResponse.json(result);
235 }
236
237 return yield* HttpServerResponse.json(
238 { error: "Not found", method: request.method, pathname },
239 { status: 404 },
240 );
241 }).pipe(Effect.orDie),
242 };
243 }).pipe(
244 Effect.provide(
245 Layer.mergeAll(
246 ECRPublic.DescribeImagesHttp,
247 ECRPublic.DescribeImageTagsHttp,
248 ECRPublic.GetRepositoryCatalogDataHttp,
249 ECRPublic.BatchCheckLayerAvailabilityHttp,
250 ECRPublic.BatchDeleteImageHttp,
251 ECRPublic.InitiateLayerUploadHttp,
252 ECRPublic.UploadLayerPartHttp,
253 ECRPublic.CompleteLayerUploadHttp,
254 ECRPublic.PutImageHttp,
255 ECRPublic.GetAuthorizationTokenHttp,
256 ECRPublic.DescribeRegistriesHttp,
257 ECRPublic.GetRegistryCatalogDataHttp,
258 ),
259 ),
260 ),
261);