1import * as eventbridge from "@distilled.cloud/aws/eventbridge";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8
9export interface PermissionProps {
10 /**
11 * Event bus name. Defaults to the account default bus.
12 */
13 eventBusName?: string;
14 /**
15 * The action that EventBridge allows for the principal.
16 * @default "events:PutEvents"
17 */
18 action?: string;
19 /**
20 * The AWS account ID, organization ID, or `*` principal receiving access.
21 */
22 principal: string;
23 /**
24 * Optional statement identifier. If omitted, Alchemy generates one.
25 */
26 statementId?: string;
27 /**
28 * Optional condition limiting the allowed caller.
29 */
30 condition?: eventbridge.Condition;
31}
32
33/**
34 * An EventBridge event bus permission statement.
35 *
36 * `Permission` manages a single `PutPermission` / `RemovePermission` lifecycle
37 * entry on an event bus so helper surfaces can safely grant publishers access
38 * without requiring callers to hand-write raw bus policies.
39 * ### Granting Access
40 * **Example:** Allow Another Account To Publish
41 * ```typescript
42 * const permission = yield* Permission("PartnerPublish", {
43 * eventBusName: bus.eventBusName,
44 * principal: "123456789012",
45 * });
46 * ```
47 *
48 * @resource
49 */
50export interface Permission extends Resource<
51 "AWS.EventBridge.Permission",
52 PermissionProps,
53 {
54 /** The statement ID identifying the permission on the bus policy. */
55 statementId: string;
56 /** The name of the event bus the permission is attached to. */
57 eventBusName: string;
58 },
59 never,
60 Providers
61> {}
62
63export const Permission = Resource<Permission>("AWS.EventBridge.Permission");
64
65export const PermissionProvider = () =>
66 Provider.effect(
67 Permission,
68 Effect.gen(function* () {
69 const toStatementId = (id: string, props: PermissionProps) =>
70 props.statementId
71 ? Effect.succeed(props.statementId)
72 : createPhysicalName({
73 id,
74 maxLength: 64,
75 });
76
77 type PermissionAttrs = { statementId: string; eventBusName: string };
78
79 return {
80 stables: ["statementId", "eventBusName"],
81 list: () =>
82 Effect.gen(function* () {
83 // EventBridge has no list-permissions API. A Permission is a
84 // single statement inside an event bus resource policy, so we
85 // enumerate every bus in the ambient account/region (manual
86 // NextToken pagination — listEventBuses is not a paginated
87 // distilled op), describe each bus to read its Policy JSON, and
88 // emit one Attributes per statement Sid.
89 const busNames: string[] = [];
90 let nextToken: string | undefined;
91 do {
92 const page = yield* eventbridge.listEventBuses({
93 NextToken: nextToken,
94 });
95 for (const bus of page.EventBuses ?? []) {
96 if (bus.Name) busNames.push(bus.Name);
97 }
98 nextToken = page.NextToken;
99 } while (nextToken);
100
101 const perBus = yield* Effect.forEach(
102 busNames,
103 (busName) =>
104 Effect.gen(function* () {
105 const res = yield* eventbridge.describeEventBus({
106 Name: busName,
107 });
108 if (!res.Policy) return [] as PermissionAttrs[];
109 const policy = yield* Effect.try({
110 try: () =>
111 JSON.parse(res.Policy!) as {
112 Statement?: { Sid?: string } | { Sid?: string }[];
113 },
114 catch: (cause) => new Error("invalid policy", { cause }),
115 }).pipe(
116 // A malformed/non-JSON policy yields no permissions
117 // rather than failing the whole enumeration.
118 Effect.orElseSucceed(() => ({
119 Statement: [] as { Sid?: string }[],
120 })),
121 );
122 const statements = Array.isArray(policy.Statement)
123 ? policy.Statement
124 : policy.Statement
125 ? [policy.Statement]
126 : [];
127 return statements
128 .filter(
129 (s): s is { Sid: string } => typeof s.Sid === "string",
130 )
131 .map((s): PermissionAttrs => ({
132 statementId: s.Sid,
133 eventBusName: busName,
134 }));
135 }).pipe(
136 // Bus removed out of band between list and describe — skip.
137 Effect.catchTag("ResourceNotFoundException", () =>
138 Effect.succeed([] as PermissionAttrs[]),
139 ),
140 ),
141 { concurrency: 10 },
142 );
143 return perBus.flat();
144 }),
145 diff: Effect.fn(function* ({ id, olds, news }) {
146 if (!isResolved(news)) return;
147 const oldStatementId = yield* toStatementId(id, olds);
148 const newStatementId = yield* toStatementId(id, news);
149
150 if (oldStatementId !== newStatementId) {
151 return { action: "replace" } as const;
152 }
153
154 if (
155 (olds.eventBusName ?? "default") !==
156 (news.eventBusName ?? "default")
157 ) {
158 return { action: "replace" } as const;
159 }
160 }),
161 reconcile: Effect.fn(function* ({ id, news, output, session }) {
162 const statementId =
163 output?.statementId ?? (yield* toStatementId(id, news));
164 const eventBusName =
165 output?.eventBusName ?? news.eventBusName ?? "default";
166 const eventBusParam =
167 eventBusName !== "default" ? eventBusName : undefined;
168
169 // Observe + Ensure for an existence-only resource: there is no
170 // server-side `describePermission` for a single statement, so we
171 // remove-then-put unconditionally. `removePermission` tolerates
172 // missing statements; `putPermission` is idempotent on identical
173 // params and overwrites on different ones — this is the official
174 // recommended update path.
175 yield* eventbridge
176 .removePermission({
177 EventBusName: eventBusParam,
178 StatementId: statementId,
179 })
180 .pipe(
181 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
182 );
183
184 yield* eventbridge.putPermission({
185 EventBusName: eventBusParam,
186 Action: news.action ?? "events:PutEvents",
187 Principal: news.principal,
188 StatementId: statementId,
189 Condition: news.condition,
190 });
191
192 yield* session.note(`EventBridge permission ${statementId}`);
193
194 return {
195 statementId,
196 eventBusName,
197 };
198 }),
199 delete: Effect.fn(function* ({ output }) {
200 yield* eventbridge
201 .removePermission({
202 EventBusName:
203 output.eventBusName !== "default"
204 ? output.eventBusName
205 : undefined,
206 StatementId: output.statementId,
207 })
208 .pipe(
209 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
210 );
211 }),
212 };
213 }),
214 );