1import * as Config from "@/AWS/Config";
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 Stream from "effect/Stream";
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, "handler.ts");
12
13export class ConfigTestFunction extends Lambda.Function<Lambda.Function>()(
14 "ConfigTestFunction",
15) {}
16
17/**
18 * Every route answers `{ …fields }` on success or `{ errorTag }` when the
19 * operation fails with a TYPED error — the test asserts the tag is in a
20 * route-specific allowlist, which proves both the binding wiring and the
21 * IAM grant. An untyped error crashes into a 500 instead.
22 */
23const errorTagged = <A, E extends { _tag: string }, R>(
24 effect: Effect.Effect<A, E, R>,
25): Effect.Effect<A | { errorTag: string; errorMessage?: string }, never, R> =>
26 effect.pipe(
27 Effect.map((a): A | { errorTag: string; errorMessage?: string } => a),
28 Effect.catch((e) =>
29 Effect.succeed({
30 errorTag: e._tag,
31 errorMessage:
32 (e as { Message?: string }).Message ??
33 (e as { message?: string }).message,
34 }),
35 ),
36 );
37
38// One binding per Config capability. DeliverConfigSnapshot has no route
39// here: it binds to the account's singleton DeliveryChannel, whose
40// lifecycle is gated behind AWS_TEST_CONFIG_RECORDER (see
41// ConfigurationRecorder.test.ts) — deploying a second channel ungated
42// would clobber the account setup.
43export default ConfigTestFunction.make(
44 {
45 main,
46 functionUrl: true,
47 // Several routes fan out Config API calls — AWS's 3s default
48 // intermittently times out under cold starts.
49 timeout: Duration.seconds(30),
50 },
51 Effect.gen(function* () {
52 // A managed rule the rule-scoped bindings are bound to. Requires a
53 // configuration recorder in the account/region — the test's beforeAll
54 // stands one up out-of-band before deploying this fixture.
55 const rule = yield* Config.ConfigRule("BindingRule", {
56 description: "alchemy config-bindings fixture rule",
57 source: {
58 owner: "AWS",
59 sourceIdentifier: "S3_BUCKET_VERSIONING_ENABLED",
60 },
61 scope: { complianceResourceTypes: ["AWS::S3::Bucket"] },
62 });
63
64 // Event source: subscribe the host to Config compliance-change events.
65 // The deploy proves the EventBridge rule + invoke permission wiring.
66 yield* Config.consumeConfigEvents({ kinds: ["compliance"] }, (events) =>
67 Stream.runForEach(events, (event) =>
68 Effect.log(
69 `config compliance: ${event.detail.configRuleName} -> ${event.detail.resourceId}`,
70 ),
71 ),
72 );
73
74 // Account-level bindings.
75 const batchGetResourceConfig = yield* Config.BatchGetResourceConfig();
76 const getResourceConfigHistory = yield* Config.GetResourceConfigHistory();
77 const listDiscoveredResources = yield* Config.ListDiscoveredResources();
78 const getDiscoveredResourceCounts =
79 yield* Config.GetDiscoveredResourceCounts();
80 const selectResourceConfig = yield* Config.SelectResourceConfig();
81 const describeComplianceByConfigRule =
82 yield* Config.DescribeComplianceByConfigRule();
83 const describeComplianceByResource =
84 yield* Config.DescribeComplianceByResource();
85 const getComplianceDetailsByResource =
86 yield* Config.GetComplianceDetailsByResource();
87 const getComplianceSummaryByConfigRule =
88 yield* Config.GetComplianceSummaryByConfigRule();
89 const getComplianceSummaryByResourceType =
90 yield* Config.GetComplianceSummaryByResourceType();
91 const describeConfigRuleEvaluationStatus =
92 yield* Config.DescribeConfigRuleEvaluationStatus();
93 const putEvaluations = yield* Config.PutEvaluations();
94 const putResourceConfig = yield* Config.PutResourceConfig();
95 const deleteResourceConfig = yield* Config.DeleteResourceConfig();
96 const startResourceEvaluation = yield* Config.StartResourceEvaluation();
97 const getResourceEvaluationSummary =
98 yield* Config.GetResourceEvaluationSummary();
99 const listResourceEvaluations = yield* Config.ListResourceEvaluations();
100
101 // Rule-scoped bindings (the rule name is injected automatically).
102 const getComplianceDetailsByConfigRule =
103 yield* Config.GetComplianceDetailsByConfigRule(rule);
104 const startConfigRulesEvaluation =
105 yield* Config.StartConfigRulesEvaluation(rule);
106 const putExternalEvaluation = yield* Config.PutExternalEvaluation(rule);
107
108 const RuleName = yield* rule.configRuleName;
109
110 return {
111 fetch: Effect.gen(function* () {
112 const request = yield* HttpServerRequest;
113 const url = new URL(request.originalUrl);
114 const route = `${request.method} ${url.pathname}`;
115
116 switch (route) {
117 case "GET /health":
118 return HttpServerResponse.text("ok");
119
120 // ── Discovering resources / querying ──────────────────────────
121
122 case "GET /select-resource-config": {
123 const result = yield* selectResourceConfig({
124 Expression:
125 "SELECT resourceId WHERE resourceType = 'AWS::S3::Bucket'",
126 });
127 return yield* HttpServerResponse.json({
128 results: result.Results ?? [],
129 });
130 }
131 case "GET /list-discovered-resources": {
132 const result = yield* listDiscoveredResources({
133 resourceType: "AWS::S3::Bucket",
134 });
135 return yield* HttpServerResponse.json({
136 identifiers: result.resourceIdentifiers ?? [],
137 });
138 }
139 case "GET /get-discovered-resource-counts": {
140 const result = yield* getDiscoveredResourceCounts();
141 return yield* HttpServerResponse.json({
142 total: result.totalDiscoveredResources ?? 0,
143 counts: result.resourceCounts ?? [],
144 });
145 }
146 case "POST /batch-get-resource-config": {
147 const result = yield* batchGetResourceConfig({
148 resourceKeys: [
149 {
150 resourceType: "AWS::S3::Bucket",
151 resourceId: "alchemy-nonexistent-bucket-probe",
152 },
153 ],
154 });
155 return yield* HttpServerResponse.json({
156 items: result.baseConfigurationItems ?? [],
157 unprocessed: result.unprocessedResourceKeys ?? [],
158 });
159 }
160 case "GET /get-resource-config-history": {
161 // The probe resource is never discovered — the typed
162 // ResourceNotDiscoveredException proves binding + IAM.
163 const result = yield* errorTagged(
164 getResourceConfigHistory({
165 resourceType: "AWS::S3::Bucket",
166 resourceId: "alchemy-nonexistent-bucket-probe",
167 }),
168 );
169 return yield* HttpServerResponse.json(
170 "errorTag" in result
171 ? result
172 : { items: result.configurationItems ?? [] },
173 );
174 }
175
176 // ── Compliance reads ───────────────────────────────────────────
177
178 case "GET /describe-compliance-by-config-rule": {
179 const ruleName = yield* RuleName;
180 const result = yield* describeComplianceByConfigRule({
181 ConfigRuleNames: [ruleName],
182 });
183 return yield* HttpServerResponse.json({
184 ruleName,
185 compliances: result.ComplianceByConfigRules ?? [],
186 });
187 }
188 case "GET /describe-compliance-by-resource": {
189 const result = yield* describeComplianceByResource({
190 ResourceType: "AWS::S3::Bucket",
191 });
192 return yield* HttpServerResponse.json({
193 compliances: result.ComplianceByResources ?? [],
194 });
195 }
196 case "GET /get-compliance-details-by-resource": {
197 const result = yield* getComplianceDetailsByResource({
198 ResourceType: "AWS::S3::Bucket",
199 ResourceId: "alchemy-nonexistent-bucket-probe",
200 });
201 return yield* HttpServerResponse.json({
202 evaluations: result.EvaluationResults ?? [],
203 });
204 }
205 case "GET /get-compliance-summary-by-config-rule": {
206 const result = yield* getComplianceSummaryByConfigRule();
207 return yield* HttpServerResponse.json({
208 summary: result.ComplianceSummary ?? null,
209 });
210 }
211 case "GET /get-compliance-summary-by-resource-type": {
212 const result = yield* getComplianceSummaryByResourceType();
213 return yield* HttpServerResponse.json({
214 summaries: result.ComplianceSummariesByResourceType ?? [],
215 });
216 }
217 case "GET /get-compliance-details-by-config-rule": {
218 const result = yield* getComplianceDetailsByConfigRule();
219 return yield* HttpServerResponse.json({
220 ruleName: yield* RuleName,
221 evaluations: result.EvaluationResults ?? [],
222 });
223 }
224
225 // ── Rule evaluation ────────────────────────────────────────────
226
227 case "GET /describe-config-rule-evaluation-status": {
228 const ruleName = yield* RuleName;
229 const result = yield* describeConfigRuleEvaluationStatus({
230 ConfigRuleNames: [ruleName],
231 });
232 return yield* HttpServerResponse.json({
233 ruleName,
234 statuses: (result.ConfigRulesEvaluationStatus ?? []).map(
235 (s) => s.ConfigRuleName,
236 ),
237 });
238 }
239 case "POST /start-config-rules-evaluation": {
240 // Back-to-back runs can reject with ResourceInUseException /
241 // LimitExceededException — typed either way.
242 const result = yield* errorTagged(startConfigRulesEvaluation());
243 return yield* HttpServerResponse.json(
244 "errorTag" in result ? result : { ok: true },
245 );
246 }
247 case "POST /put-evaluations": {
248 // TestMode validates the evaluations without pushing them; an
249 // out-of-band token can still reject with the typed
250 // InvalidResultTokenException.
251 const result = yield* errorTagged(
252 putEvaluations({
253 ResultToken: "alchemy-config-bindings-test-token",
254 TestMode: true,
255 Evaluations: [
256 {
257 ComplianceResourceType: "AWS::S3::Bucket",
258 ComplianceResourceId: "alchemy-nonexistent-bucket-probe",
259 ComplianceType: "COMPLIANT",
260 OrderingTimestamp: new Date(),
261 },
262 ],
263 }),
264 );
265 return yield* HttpServerResponse.json(
266 "errorTag" in result
267 ? result
268 : { failed: result.FailedEvaluations ?? [] },
269 );
270 }
271 case "POST /put-external-evaluation": {
272 // The fixture rule is a managed rule, not an external-evaluation
273 // rule — the typed InvalidParameterValueException proves binding
274 // + IAM.
275 const result = yield* errorTagged(
276 putExternalEvaluation({
277 ExternalEvaluation: {
278 ComplianceResourceType: "AWS::S3::Bucket",
279 ComplianceResourceId: "alchemy-nonexistent-bucket-probe",
280 ComplianceType: "NOT_APPLICABLE",
281 OrderingTimestamp: new Date(),
282 },
283 }),
284 );
285 return yield* HttpServerResponse.json(
286 "errorTag" in result ? result : { ok: true },
287 );
288 }
289
290 // ── Custom resource recording ──────────────────────────────────
291
292 case "POST /put-resource-config": {
293 // Recording a third-party type requires a running recorder —
294 // NoRunningConfigurationRecorderException is typed and proves
295 // binding + IAM when the fixture recorder is stopped.
296 const result = yield* errorTagged(
297 putResourceConfig({
298 ResourceType: "MyCompany::Alchemy::Fixture",
299 SchemaVersionId: "1.0",
300 ResourceId: "alchemy-config-bindings-fixture",
301 Configuration: JSON.stringify({ fixture: true }),
302 }),
303 );
304 return yield* HttpServerResponse.json(
305 "errorTag" in result ? result : { ok: true },
306 );
307 }
308 case "POST /delete-resource-config": {
309 const result = yield* errorTagged(
310 deleteResourceConfig({
311 ResourceType: "MyCompany::Alchemy::Fixture",
312 ResourceId: "alchemy-config-bindings-fixture",
313 }),
314 );
315 return yield* HttpServerResponse.json(
316 "errorTag" in result ? result : { ok: true },
317 );
318 }
319
320 // ── Proactive resource evaluation ──────────────────────────────
321
322 case "POST /start-resource-evaluation": {
323 const result = yield* errorTagged(
324 startResourceEvaluation({
325 EvaluationMode: "PROACTIVE",
326 ResourceDetails: {
327 ResourceId: "alchemy-config-bindings-proactive",
328 ResourceType: "AWS::S3::Bucket",
329 ResourceConfiguration: JSON.stringify({
330 BucketName: "alchemy-config-bindings-proactive",
331 }),
332 ResourceConfigurationSchemaType: "CFN_RESOURCE_SCHEMA",
333 },
334 }),
335 );
336 return yield* HttpServerResponse.json(
337 "errorTag" in result
338 ? result
339 : { id: result.ResourceEvaluationId },
340 );
341 }
342 case "GET /get-resource-evaluation-summary": {
343 const id = url.searchParams.get("id") ?? "";
344 const result = yield* errorTagged(
345 getResourceEvaluationSummary({ ResourceEvaluationId: id }),
346 );
347 return yield* HttpServerResponse.json(
348 "errorTag" in result
349 ? result
350 : {
351 id: result.ResourceEvaluationId,
352 status: result.EvaluationStatus?.Status,
353 },
354 );
355 }
356 case "GET /list-resource-evaluations": {
357 const result = yield* listResourceEvaluations({
358 Filters: { EvaluationMode: "PROACTIVE" },
359 });
360 return yield* HttpServerResponse.json({
361 evaluations: result.ResourceEvaluations ?? [],
362 });
363 }
364
365 default:
366 return yield* HttpServerResponse.json(
367 { error: "Not found", route },
368 { status: 404 },
369 );
370 }
371 }).pipe(Effect.orDie),
372 };
373 }).pipe(
374 Effect.provide(
375 Layer.mergeAll(
376 Lambda.EventSource,
377 Config.BatchGetResourceConfigHttp,
378 Config.DeleteResourceConfigHttp,
379 Config.DescribeComplianceByConfigRuleHttp,
380 Config.DescribeComplianceByResourceHttp,
381 Config.DescribeConfigRuleEvaluationStatusHttp,
382 Config.GetComplianceDetailsByConfigRuleHttp,
383 Config.GetComplianceDetailsByResourceHttp,
384 Config.GetComplianceSummaryByConfigRuleHttp,
385 Config.GetComplianceSummaryByResourceTypeHttp,
386 Config.GetDiscoveredResourceCountsHttp,
387 Config.GetResourceConfigHistoryHttp,
388 Config.GetResourceEvaluationSummaryHttp,
389 Config.ListDiscoveredResourcesHttp,
390 Config.ListResourceEvaluationsHttp,
391 Config.PutEvaluationsHttp,
392 Config.PutExternalEvaluationHttp,
393 Config.PutResourceConfigHttp,
394 Config.SelectResourceConfigHttp,
395 Config.StartConfigRulesEvaluationHttp,
396 Config.StartResourceEvaluationHttp,
397 ),
398 ),
399 ),
400);