1import * as Lambda from "@/AWS/Lambda";
2import * as ObservabilityAdmin from "@/AWS/ObservabilityAdmin";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Result from "effect/Result";
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 ObservabilityAdminBindingsFunction extends Lambda.Function<Lambda.Function>()(
14 "ObservabilityAdminBindingsFunction",
15) {}
16
17export default ObservabilityAdminBindingsFunction.make(
18 {
19 main,
20 functionUrl: true,
21 timeout: Duration.seconds(30),
22 },
23 Effect.gen(function* () {
24 // A short-lived rule exercising the rule-scoped grant + name injection.
25 // It exists for well under the ~24h AWS Config discovery window, so it
26 // never actually configures flow logs. (Requires the account to be
27 // onboarded to telemetry config; the test's beforeAll ensures that
28 // out-of-band.)
29 const rule = yield* ObservabilityAdmin.TelemetryRule("BindingsRule", {
30 resourceType: "AWS::EC2::VPC",
31 telemetryType: "Logs",
32 telemetrySourceTypes: ["VPC_FLOW_LOGS"],
33 destinationConfiguration: {
34 DestinationType: "cloud-watch-logs",
35 Retention: "30 days",
36 },
37 });
38
39 const bound = {
40 listResourceTelemetry: yield* ObservabilityAdmin.ListResourceTelemetry(),
41 getTelemetryEvaluationStatus:
42 yield* ObservabilityAdmin.GetTelemetryEvaluationStatus(),
43 getTelemetryEnrichmentStatus:
44 yield* ObservabilityAdmin.GetTelemetryEnrichmentStatus(),
45 listTelemetryRules: yield* ObservabilityAdmin.ListTelemetryRules(),
46 getTelemetryRule: yield* ObservabilityAdmin.GetTelemetryRule(rule),
47 };
48
49 return {
50 fetch: Effect.gen(function* () {
51 const request = yield* HttpServerRequest;
52 const url = new URL(request.originalUrl);
53 const pathname = url.pathname;
54
55 if (request.method === "GET" && pathname === "/bindings") {
56 return yield* HttpServerResponse.json({
57 bound: Object.keys(bound),
58 });
59 }
60
61 // Account-level telemetry audit data plane. Right after onboarding
62 // the audit backend may answer with a service-side error until
63 // AWS Config discovery warms up (documented as up to 24h), so
64 // report the typed tag instead of dying — an IAM gap would surface
65 // as AccessDeniedException, which the test rejects.
66 if (request.method === "GET" && pathname === "/resource-telemetry") {
67 const result = yield* Effect.result(
68 bound.listResourceTelemetry({
69 ResourceTypes: ["AWS::EC2::VPC"],
70 MaxResults: 10,
71 }),
72 );
73 return yield* HttpServerResponse.json(
74 Result.isSuccess(result)
75 ? {
76 tag: "ok",
77 count: (result.success.TelemetryConfigurations ?? []).length,
78 }
79 : { tag: result.failure._tag, error: String(result.failure) },
80 );
81 }
82
83 // Account onboarding status.
84 if (request.method === "GET" && pathname === "/evaluation-status") {
85 const { Status } = yield* bound.getTelemetryEvaluationStatus();
86 return yield* HttpServerResponse.json({
87 status: Status ?? "NOT_STARTED",
88 });
89 }
90
91 // Enrichment status — a never-onboarded account answers with a typed
92 // ResourceNotFoundException, which still proves the grant.
93 if (request.method === "GET" && pathname === "/enrichment-status") {
94 const status = yield* bound.getTelemetryEnrichmentStatus().pipe(
95 Effect.map((r) => r.Status ?? "Stopped"),
96 Effect.catchTag("ResourceNotFoundException", () =>
97 Effect.succeed("NotOnboarded"),
98 ),
99 );
100 return yield* HttpServerResponse.json({ status });
101 }
102
103 // Account-level rule enumeration — must include the bound rule.
104 if (request.method === "GET" && pathname === "/rules") {
105 const { TelemetryRuleSummaries } = yield* bound.listTelemetryRules();
106 return yield* HttpServerResponse.json({
107 names: (TelemetryRuleSummaries ?? []).flatMap((summary) =>
108 summary.RuleName === undefined ? [] : [summary.RuleName],
109 ),
110 });
111 }
112
113 // Rule-scoped read — the rule name is injected from the binding.
114 if (request.method === "GET" && pathname === "/rule") {
115 const got = yield* bound.getTelemetryRule();
116 return yield* HttpServerResponse.json({
117 ruleName: got.RuleName,
118 telemetryType: got.TelemetryRule?.TelemetryType,
119 retentionInDays:
120 got.TelemetryRule?.DestinationConfiguration?.RetentionInDays,
121 });
122 }
123
124 return yield* HttpServerResponse.json(
125 { error: "Not found", method: request.method, pathname },
126 { status: 404 },
127 );
128 }).pipe(Effect.orDie),
129 };
130 }).pipe(
131 Effect.provide(
132 Layer.mergeAll(
133 ObservabilityAdmin.ListResourceTelemetryHttp,
134 ObservabilityAdmin.GetTelemetryEvaluationStatusHttp,
135 ObservabilityAdmin.GetTelemetryEnrichmentStatusHttp,
136 ObservabilityAdmin.ListTelemetryRulesHttp,
137 ObservabilityAdmin.GetTelemetryRuleHttp,
138 ),
139 ),
140 ),
141);