1import type * as Effect from "effect/Effect";
2import type * as Stream from "effect/Stream";
3import {
4 consumeBusEvents,
5 type EventRecord,
6 type EventRouteProps,
7} from "../EventBridge/EventSource.ts";
8
9/**
10 * The `detail` payload EventBridge delivers for a CloudTrail-recorded API
11 * call (`AWS API Call via CloudTrail`). The shape mirrors the CloudTrail
12 * event record.
13 */
14export interface ApiCallDetail {
15 /** The service endpoint the call was made to, e.g. `s3.amazonaws.com`. */
16 eventSource: string;
17 /** The API operation name, e.g. `PutBucketTagging`. */
18 eventName: string;
19 /** The region the call was made in. */
20 awsRegion?: string;
21 /** Unique id of the CloudTrail event record. */
22 eventID?: string;
23 /** ISO timestamp of the call. */
24 eventTime?: string;
25 /** Version of the CloudTrail event schema. */
26 eventVersion?: string;
27 /** Identity that made the call (IAM user, role session, service, …). */
28 userIdentity?: Record<string, unknown>;
29 /** The request parameters of the call, as recorded by CloudTrail. */
30 requestParameters?: Record<string, any>;
31 /** The response elements of the call (mutating calls only). */
32 responseElements?: Record<string, any> | null;
33 /** Error code when the recorded call failed. */
34 errorCode?: string;
35 /** Error message when the recorded call failed. */
36 errorMessage?: string;
37 /** Additional event fields (the schema grows over time). */
38 [key: string]: unknown;
39}
40
41/** A CloudTrail API-call EventBridge event delivered to the handler. */
42export type ApiCallEvent = EventRecord<ApiCallDetail>;
43
44export interface ApiCallEventsProps extends EventRouteProps {
45 /**
46 * Logical id for the backing EventBridge rule.
47 * @default "CloudTrailApiCallEvents"
48 */
49 id?: string;
50 /**
51 * Only deliver calls made to these service endpoints (the CloudTrail
52 * `eventSource`, e.g. `["s3.amazonaws.com"]`).
53 * @default all services
54 */
55 eventSources?: string[];
56 /**
57 * Only deliver these API operations (the CloudTrail `eventName`, e.g.
58 * `["PutBucketTagging"]`).
59 * @default all operations
60 */
61 eventNames?: string[];
62}
63
64/**
65 * Event source connecting CloudTrail-recorded API calls to the hosting
66 * compute. EventBridge receives every **mutating** management API call
67 * CloudTrail records (detail-type `AWS API Call via CloudTrail`) on the
68 * account's default bus — read-only calls (`Get*`/`List*`/`Describe*`) and
69 * data events are not delivered. This subscribes the host Function to those
70 * events so it can react to control-plane changes (security automation,
71 * config auditing) without polling.
72 *
73 * EventBridge receives these events automatically — no trail is required and
74 * no additional resource is created besides the EventBridge rule targeting
75 * the host. Provide the host-specific implementation layer (e.g.
76 * `AWS.Lambda.EventSource`) on the Function effect.
77 *
78 * ### Consuming API Call Events
79 * **Example:** React to S3 Bucket Configuration Changes
80 * ```typescript
81 * import * as AWS from "alchemy/AWS";
82 *
83 * export default AuditFunction.make(
84 * { main: import.meta.url },
85 * Effect.gen(function* () {
86 * yield* AWS.CloudTrail.consumeApiCallEvents(
87 * {
88 * eventSources: ["s3.amazonaws.com"],
89 * eventNames: ["PutBucketPolicy", "PutBucketAcl"],
90 * },
91 * (events) =>
92 * Stream.runForEach(events, (event) =>
93 * Effect.log(
94 * `${event.detail.eventName} on ${JSON.stringify(
95 * event.detail.requestParameters,
96 * )}`,
97 * ),
98 * ),
99 * );
100 * return {};
101 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
102 * );
103 * ```
104 */
105export const consumeApiCallEvents = <StreamReq = never, Req = never>(
106 props: ApiCallEventsProps,
107 process: (
108 events: Stream.Stream<ApiCallEvent, never, StreamReq>,
109 ) => Effect.Effect<void, never, Req>,
110) =>
111 consumeBusEvents(
112 props.id ?? "CloudTrailApiCallEvents",
113 {
114 "detail-type": ["AWS API Call via CloudTrail"],
115 ...(props.eventSources || props.eventNames
116 ? {
117 detail: {
118 ...(props.eventSources
119 ? { eventSource: [...props.eventSources] }
120 : {}),
121 ...(props.eventNames ? { eventName: [...props.eventNames] } : {}),
122 },
123 }
124 : {}),
125 },
126 { description: props.description, state: props.state },
127 process,
128 );