1import * as Lambda from "@/AWS/Lambda";
2import * as Shield from "@/AWS/Shield";
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 ShieldTestFunction extends Lambda.Function<Lambda.Function>()(
14 "ShieldTestFunction",
15) {}
16
17/**
18 * Routes answer `{ …fields }` on success or `{ errorTag }` when the operation
19 * fails with a TYPED error — the test asserts the tag is in a route-specific
20 * allowlist, which proves both the binding wiring and the IAM grant. An
21 * 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// The testing account is NOT (and must never be) subscribed to Shield
39// Advanced ($3,000/month, 1-year commitment). GetSubscriptionState and
40// DescribeAttackStatistics succeed on any account; the subscription-gated
41// operations answer with their typed entitlement/not-found tags, which the
42// test asserts — proving the binding + IAM wiring end-to-end either way.
43export default ShieldTestFunction.make(
44 {
45 main,
46 functionUrl: true,
47 timeout: Duration.seconds(30),
48 },
49 Effect.gen(function* () {
50 // Event source: subscribe the host to Shield Advanced attack events
51 // (AWS Health, source `aws.health`, service SHIELD). The deploy proves
52 // the EventBridge rule + invoke permission wiring; events only fire on
53 // subscribed accounts under active attack.
54 yield* Shield.consumeAttackEvents({}, (events) =>
55 Stream.runForEach(events, (event) =>
56 Effect.log(`shield attack event: ${event.detail.eventTypeCode}`),
57 ),
58 );
59
60 const getSubscriptionState = yield* Shield.GetSubscriptionState();
61 const describeAttackStatistics = yield* Shield.DescribeAttackStatistics();
62 const listAttacks = yield* Shield.ListAttacks();
63 const describeAttack = yield* Shield.DescribeAttack();
64 const describeDRTAccess = yield* Shield.DescribeDRTAccess();
65 const listResourcesInProtectionGroup =
66 yield* Shield.ListResourcesInProtectionGroup();
67
68 const bound = {
69 getSubscriptionState,
70 describeAttackStatistics,
71 listAttacks,
72 describeAttack,
73 describeDRTAccess,
74 listResourcesInProtectionGroup,
75 };
76
77 return {
78 fetch: Effect.gen(function* () {
79 const request = yield* HttpServerRequest;
80 const url = new URL(request.originalUrl);
81 const pathname = url.pathname;
82
83 if (request.method === "GET" && pathname === "/bindings") {
84 return yield* HttpServerResponse.json({ bound: Object.keys(bound) });
85 }
86
87 // Available to every account, subscribed or not.
88 if (request.method === "GET" && pathname === "/subscription-state") {
89 const { SubscriptionState } = yield* getSubscriptionState();
90 return yield* HttpServerResponse.json({ state: SubscriptionState });
91 }
92
93 // Available to Standard and Advanced customers alike.
94 if (request.method === "GET" && pathname === "/attack-stats") {
95 const result = yield* errorTagged(
96 describeAttackStatistics().pipe(
97 Effect.map((r) => ({ dataItems: r.DataItems.length })),
98 ),
99 );
100 return yield* HttpServerResponse.json(result);
101 }
102
103 // Subscription-gated — a non-subscribed account answers with a typed
104 // error tag.
105 if (request.method === "GET" && pathname === "/attacks") {
106 const result = yield* errorTagged(
107 listAttacks().pipe(
108 Effect.map((r) => ({
109 count: (r.AttackSummaries ?? []).length,
110 })),
111 ),
112 );
113 return yield* HttpServerResponse.json(result);
114 }
115
116 // A nonexistent attack id answers with an empty document (observed
117 // live) or the documented typed AccessDeniedException.
118 if (request.method === "GET" && pathname === "/attack-detail") {
119 const result = yield* errorTagged(
120 describeAttack({
121 AttackId: "00000000-0000-0000-0000-000000000000",
122 }).pipe(Effect.map((r) => ({ attackId: r.Attack?.AttackId }))),
123 );
124 return yield* HttpServerResponse.json(result);
125 }
126
127 // Subscription-gated — a non-subscribed account answers with the
128 // typed ResourceNotFoundException.
129 if (request.method === "GET" && pathname === "/drt-access") {
130 const result = yield* errorTagged(
131 describeDRTAccess().pipe(
132 Effect.map((r) => ({
133 roleArn: r.RoleArn ?? null,
134 logBuckets: (r.LogBucketList ?? []).length,
135 })),
136 ),
137 );
138 return yield* HttpServerResponse.json(result);
139 }
140
141 // A nonexistent group answers with the typed
142 // ResourceNotFoundException (even without a subscription).
143 if (request.method === "GET" && pathname === "/group-members") {
144 const result = yield* errorTagged(
145 listResourcesInProtectionGroup({
146 ProtectionGroupId: "alchemy-shield-bindings-nonexistent",
147 }).pipe(
148 Effect.map((r) => ({
149 count: (r.ResourceArns ?? []).length,
150 })),
151 ),
152 );
153 return yield* HttpServerResponse.json(result);
154 }
155
156 return yield* HttpServerResponse.json(
157 { error: "Not found", method: request.method, pathname },
158 { status: 404 },
159 );
160 }).pipe(
161 // Surface typed operation errors as JSON so live-test failures carry
162 // the tag + message instead of an opaque 500 (routes that expect a
163 // typed error use `errorTagged` above and never reach this).
164 Effect.catch((e) =>
165 HttpServerResponse.json(
166 {
167 errorTag: (e as { _tag?: string })._tag ?? "UnknownError",
168 errorMessage:
169 (e as { message?: string }).message ??
170 (e as { Message?: string }).Message,
171 },
172 { status: 500 },
173 ),
174 ),
175 Effect.orDie,
176 ),
177 };
178 }).pipe(
179 Effect.provide(
180 Layer.mergeAll(
181 Lambda.EventSource,
182 Shield.GetSubscriptionStateHttp,
183 Shield.DescribeAttackStatisticsHttp,
184 Shield.ListAttacksHttp,
185 Shield.DescribeAttackHttp,
186 Shield.DescribeDRTAccessHttp,
187 Shield.ListResourcesInProtectionGroupHttp,
188 ),
189 ),
190 ),
191);