1import * as eventbridge from "@distilled.cloud/aws/eventbridge";
2import * as Effect from "effect/Effect";
3import type * as Redacted from "effect/Redacted";
4import * as Schedule from "effect/Schedule";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import type { AccountID } from "../Environment.ts";
10import type { Providers } from "../Providers.ts";
11import type { RegionID } from "../Region.ts";
12
13export type {
14 ConnectionAuthorizationType,
15 ConnectionHttpParameters,
16 ConnectionOAuthHttpMethod,
17 ConnectionState,
18} from "@distilled.cloud/aws/eventbridge";
19
20export type ConnectionName = string;
21export type ConnectionArn =
22 `arn:aws:events:${RegionID}:${AccountID}:connection/${ConnectionName}/${string}`;
23
24export interface ConnectionApiKeyAuthParameters {
25 /** Header name the API key is sent under (e.g. `x-api-key`). */
26 apiKeyName: string;
27 /** The API key value. Wrap with `Redacted.make(...)` — stored by EventBridge in Secrets Manager. */
28 apiKeyValue: Redacted.Redacted<string>;
29}
30
31export interface ConnectionBasicAuthParameters {
32 /** Username for HTTP Basic authorization. */
33 username: string;
34 /** Password for HTTP Basic authorization. Wrap with `Redacted.make(...)`. */
35 password: Redacted.Redacted<string>;
36}
37
38export interface ConnectionOAuthClientParameters {
39 /** OAuth client ID. */
40 clientId: string;
41 /** OAuth client secret. Wrap with `Redacted.make(...)`. */
42 clientSecret: Redacted.Redacted<string>;
43}
44
45export interface ConnectionOAuthParameters {
46 /** The client credentials exchanged for a token. */
47 clientParameters: ConnectionOAuthClientParameters;
48 /** URL of the OAuth authorization (token) endpoint. */
49 authorizationEndpoint: string;
50 /** HTTP method used against the authorization endpoint. */
51 httpMethod: eventbridge.ConnectionOAuthHttpMethod;
52 /** Additional parameters included in the token request. */
53 oauthHttpParameters?: eventbridge.ConnectionHttpParameters;
54}
55
56export interface ConnectionAuthParameters {
57 /** API-key authorization (`authorizationType: "API_KEY"`). */
58 apiKeyAuthParameters?: ConnectionApiKeyAuthParameters;
59 /** Basic authorization (`authorizationType: "BASIC"`). */
60 basicAuthParameters?: ConnectionBasicAuthParameters;
61 /** OAuth client-credentials authorization (`authorizationType: "OAUTH_CLIENT_CREDENTIALS"`). */
62 oauthParameters?: ConnectionOAuthParameters;
63 /** Additional header/query/body parameters included in every invocation. */
64 invocationHttpParameters?: eventbridge.ConnectionHttpParameters;
65}
66
67export interface ConnectionProps {
68 /**
69 * Name of the connection. Must match [\.\-_A-Za-z0-9]+, 1-64 characters.
70 * If omitted, a unique name will be generated.
71 */
72 name?: ConnectionName;
73
74 /**
75 * Description of the connection. Max 512 characters.
76 */
77 description?: string;
78
79 /**
80 * The type of authorization to use for the connection.
81 */
82 authorizationType: eventbridge.ConnectionAuthorizationType;
83
84 /**
85 * The authorization parameters matching `authorizationType`. Secret values
86 * (`apiKeyValue`, `password`, `clientSecret`) are `Redacted` end-to-end and
87 * stored by EventBridge in Secrets Manager.
88 */
89 authParameters: ConnectionAuthParameters;
90
91 /**
92 * The identifier of the KMS customer managed key to encrypt the connection
93 * secret.
94 */
95 kmsKeyIdentifier?: string;
96}
97
98/**
99 * An Amazon EventBridge connection holding the authorization used to invoke
100 * an HTTP endpoint through an {@link ApiDestination}. EventBridge stores the
101 * secret half of the connection in Secrets Manager on your behalf.
102 *
103 * Connections do not support tags, so ownership is tracked by the
104 * deterministic physical name.
105 * ### Connecting to APIs
106 * **Example:** API-Key Connection
107 * ```typescript
108 * import * as Redacted from "effect/Redacted";
109 *
110 * const connection = yield* AWS.EventBridge.Connection("PartnerApi", {
111 * authorizationType: "API_KEY",
112 * authParameters: {
113 * apiKeyAuthParameters: {
114 * apiKeyName: "x-api-key",
115 * apiKeyValue: Redacted.make(process.env.PARTNER_API_KEY!),
116 * },
117 * },
118 * });
119 * ```
120 *
121 * **Example:** OAuth Client-Credentials Connection
122 * ```typescript
123 * const connection = yield* AWS.EventBridge.Connection("OAuthApi", {
124 * authorizationType: "OAUTH_CLIENT_CREDENTIALS",
125 * authParameters: {
126 * oauthParameters: {
127 * clientParameters: {
128 * clientId: "my-client",
129 * clientSecret: Redacted.make(process.env.OAUTH_SECRET!),
130 * },
131 * authorizationEndpoint: "https://auth.example.com/oauth/token",
132 * httpMethod: "POST",
133 * },
134 * },
135 * });
136 * ```
137 *
138 * @resource
139 */
140export interface Connection extends Resource<
141 "AWS.EventBridge.Connection",
142 ConnectionProps,
143 {
144 /** The name of the connection. */
145 connectionName: ConnectionName;
146 /** The ARN of the connection. */
147 connectionArn: ConnectionArn;
148 /** The state of the connection (e.g. `AUTHORIZED`). */
149 connectionState: eventbridge.ConnectionState;
150 /** ARN of the Secrets Manager secret EventBridge created for the connection. */
151 secretArn?: string;
152 },
153 never,
154 Providers
155> {}
156export const Connection = Resource<Connection>("AWS.EventBridge.Connection");
157
158const toCreateAuthParameters = (
159 auth: ConnectionAuthParameters,
160): eventbridge.CreateConnectionAuthRequestParameters => ({
161 ApiKeyAuthParameters: auth.apiKeyAuthParameters
162 ? {
163 ApiKeyName: auth.apiKeyAuthParameters.apiKeyName,
164 ApiKeyValue: auth.apiKeyAuthParameters.apiKeyValue,
165 }
166 : undefined,
167 BasicAuthParameters: auth.basicAuthParameters
168 ? {
169 Username: auth.basicAuthParameters.username,
170 Password: auth.basicAuthParameters.password,
171 }
172 : undefined,
173 OAuthParameters: auth.oauthParameters
174 ? {
175 ClientParameters: {
176 ClientID: auth.oauthParameters.clientParameters.clientId,
177 ClientSecret: auth.oauthParameters.clientParameters.clientSecret,
178 },
179 AuthorizationEndpoint: auth.oauthParameters.authorizationEndpoint,
180 HttpMethod: auth.oauthParameters.httpMethod,
181 OAuthHttpParameters: auth.oauthParameters.oauthHttpParameters,
182 }
183 : undefined,
184 InvocationHttpParameters: auth.invocationHttpParameters,
185});
186
187export const ConnectionProvider = () =>
188 Provider.effect(
189 Connection,
190 Effect.gen(function* () {
191 const createConnectionName = (
192 id: string,
193 props: { name?: string } = {},
194 ) =>
195 props.name
196 ? Effect.succeed(props.name)
197 : createPhysicalName({
198 id,
199 maxLength: 64,
200 });
201
202 /**
203 * Poll until the connection settles out of its transitional state
204 * (CREATING/UPDATING/AUTHORIZING). API-key and basic connections
205 * authorize in seconds; the wait is bounded so a slow OAuth handshake
206 * surfaces the last observed state instead of hanging.
207 */
208 const awaitSettled = (name: string) =>
209 eventbridge.describeConnection({ Name: name }).pipe(
210 // A describe fired immediately after create can be eventually
211 // consistent — absorb NotFound briefly before polling the state.
212 Effect.retry({
213 while: (e): boolean => e._tag === "ResourceNotFoundException",
214 schedule: Schedule.spaced("1 second"),
215 times: 5,
216 }),
217 Effect.repeat({
218 schedule: Schedule.spaced("2 seconds"),
219 until: (r): boolean =>
220 r.ConnectionState !== "CREATING" &&
221 r.ConnectionState !== "UPDATING" &&
222 r.ConnectionState !== "AUTHORIZING",
223 times: 15,
224 }),
225 );
226
227 return {
228 stables: ["connectionName", "connectionArn"],
229 diff: Effect.fn(function* ({ id, news, olds }) {
230 if (!isResolved(news)) return;
231 const oldName = yield* createConnectionName(id, olds);
232 const newName = yield* createConnectionName(id, news);
233 if (oldName !== newName) {
234 return { action: "replace" } as const;
235 }
236 }),
237 read: Effect.fn(function* ({ id, olds, output }) {
238 // Connections don't support tags; the deterministic physical name
239 // is the ownership signal (it embeds app/stage/logical id).
240 const connectionName =
241 output?.connectionName ??
242 (yield* createConnectionName(id, olds ?? {}));
243 const described = yield* eventbridge
244 .describeConnection({ Name: connectionName })
245 .pipe(
246 Effect.catchTag("ResourceNotFoundException", () =>
247 Effect.succeed(undefined),
248 ),
249 );
250 if (!described?.Name || !described.ConnectionArn) {
251 return undefined;
252 }
253 return {
254 connectionName: described.Name,
255 connectionArn: described.ConnectionArn as ConnectionArn,
256 connectionState: described.ConnectionState ?? "AUTHORIZED",
257 secretArn: described.SecretArn,
258 };
259 }),
260 list: () =>
261 Effect.gen(function* () {
262 const attrs: {
263 connectionName: ConnectionName;
264 connectionArn: ConnectionArn;
265 connectionState: eventbridge.ConnectionState;
266 secretArn?: string;
267 }[] = [];
268 let nextToken: string | undefined;
269 do {
270 const page = yield* eventbridge.listConnections({
271 NextToken: nextToken,
272 });
273 for (const connection of page.Connections ?? []) {
274 if (!connection.Name || !connection.ConnectionArn) {
275 continue;
276 }
277 attrs.push({
278 connectionName: connection.Name,
279 connectionArn: connection.ConnectionArn as ConnectionArn,
280 connectionState: connection.ConnectionState ?? "AUTHORIZED",
281 });
282 }
283 nextToken = page.NextToken;
284 } while (nextToken);
285 return attrs;
286 }),
287 reconcile: Effect.fn(function* ({ id, news, output, session }) {
288 const connectionName =
289 output?.connectionName ?? (yield* createConnectionName(id, news));
290
291 // Observe — live cloud state is authoritative; a vanished
292 // connection falls through to create.
293 const observed = yield* eventbridge
294 .describeConnection({ Name: connectionName })
295 .pipe(
296 Effect.catchTag("ResourceNotFoundException", () =>
297 Effect.succeed(undefined),
298 ),
299 );
300
301 if (!observed?.ConnectionArn) {
302 // Ensure — create the connection; tolerate an AlreadyExists race
303 // with a peer reconciler and converge via the update below.
304 yield* eventbridge
305 .createConnection({
306 Name: connectionName,
307 Description: news.description,
308 AuthorizationType: news.authorizationType,
309 AuthParameters: toCreateAuthParameters(news.authParameters),
310 KmsKeyIdentifier: news.kmsKeyIdentifier,
311 })
312 .pipe(
313 Effect.catchTag(
314 "ResourceAlreadyExistsException",
315 () => Effect.void,
316 ),
317 );
318 } else {
319 // Sync — updateConnection overwrites description, authorization
320 // type/parameters, and KMS key in one shot. The update-parameter
321 // shape is structurally compatible with the create shape (all
322 // fields optional).
323 yield* eventbridge.updateConnection({
324 Name: connectionName,
325 Description: news.description,
326 AuthorizationType: news.authorizationType,
327 AuthParameters: toCreateAuthParameters(news.authParameters),
328 KmsKeyIdentifier: news.kmsKeyIdentifier,
329 });
330 }
331
332 const settled = yield* awaitSettled(connectionName);
333 const connectionArn = (settled.ConnectionArn ??
334 observed?.ConnectionArn) as ConnectionArn;
335
336 yield* session.note(connectionArn);
337 return {
338 connectionName,
339 connectionArn,
340 connectionState: settled.ConnectionState ?? "AUTHORIZED",
341 secretArn: settled.SecretArn,
342 };
343 }),
344 delete: Effect.fn(function* ({ output }) {
345 // Deleting a connection that ApiDestinations still reference fails
346 // until the destinations are gone; the engine deletes dependents
347 // first, so a bounded retry absorbs eventual consistency.
348 yield* eventbridge
349 .deleteConnection({ Name: output.connectionName })
350 .pipe(
351 Effect.retry({
352 while: (e): boolean =>
353 e._tag === "ConcurrentModificationException",
354 schedule: Schedule.spaced("2 seconds"),
355 times: 8,
356 }),
357 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
358 );
359 }),
360 };
361 }),
362 );