1import * as mm from "@distilled.cloud/aws/mailmanager";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import {
9 createInternalTags,
10 createTagsList,
11 hasAlchemyTags,
12} from "../../Tags.ts";
13import type { Providers } from "../Providers.ts";
14import { readMailManagerTags, syncMailManagerTags } from "./internal.ts";
15
16export interface AddonSubscriptionProps {
17 /**
18 * Name of the Add On product to subscribe to (e.g. `TRENDMICRO_VSAPI`,
19 * `SPAMHAUS_DBL`, `ABUSIX_MAIL_INTELLIGENCE`). Subscribing accepts the Add
20 * On's terms of use and additional pricing. Immutable — changing the name
21 * replaces the subscription.
22 */
23 addonName: string;
24 /**
25 * Tags applied to the subscription. Alchemy ownership tags are merged in
26 * automatically.
27 */
28 tags?: Record<string, string>;
29}
30
31export interface AddonSubscription extends Resource<
32 "AWS.MailManager.AddonSubscription",
33 AddonSubscriptionProps,
34 {
35 /** Server-assigned ID of the Add On subscription. */
36 addonSubscriptionId: string;
37 /** ARN of the Add On subscription. */
38 addonSubscriptionArn: string;
39 /** Name of the subscribed Add On product. */
40 addonName: string;
41 },
42 never,
43 Providers
44> {}
45
46/**
47 * An SES Mail Manager Add On subscription — the acceptance of a third-party
48 * Add On's terms of use and additional pricing. An
49 * {@link AddonInstance} created from the subscription is what rule sets and
50 * traffic policies actually reference.
51 *
52 * Subscriptions are immutable after creation (only tags update in place).
53 *
54 * :::warning
55 * Creating a subscription accepts the Add On's **additional pricing**.
56 * :::
57 * ### Subscribing to an Add On
58 * **Example:** Spamhaus DBL
59 * ```typescript
60 * import * as MailManager from "alchemy/AWS/MailManager";
61 *
62 * const subscription = yield* MailManager.AddonSubscription("Spamhaus", {
63 * addonName: "SPAMHAUS_DBL",
64 * });
65 * const instance = yield* MailManager.AddonInstance("SpamhausInstance", {
66 * addonSubscriptionId: subscription.addonSubscriptionId,
67 * });
68 * ```
69 *
70 * @resource
71 */
72export const AddonSubscription = Resource<AddonSubscription>(
73 "AWS.MailManager.AddonSubscription",
74);
75
76export const AddonSubscriptionProvider = () =>
77 Provider.effect(
78 AddonSubscription,
79 Effect.gen(function* () {
80 const getById = (addonSubscriptionId: string) =>
81 mm
82 .getAddonSubscription({ AddonSubscriptionId: addonSubscriptionId })
83 .pipe(
84 Effect.catchTag("ResourceNotFoundException", () =>
85 Effect.succeed(undefined),
86 ),
87 );
88
89 const listAll = mm.listAddonSubscriptions.pages({}).pipe(
90 Stream.runCollect,
91 Effect.map((chunk) =>
92 Array.from(chunk).flatMap((page) => page.AddonSubscriptions ?? []),
93 ),
94 );
95
96 // Subscriptions have no user-supplied name — recover identity after a
97 // lost state write by searching for our ownership tags.
98 const findByAlchemyTags = Effect.fn(function* (id: string) {
99 const subscriptions = yield* listAll;
100 for (const subscription of subscriptions) {
101 if (subscription.AddonSubscriptionArn === undefined) continue;
102 const tags = yield* readMailManagerTags(
103 subscription.AddonSubscriptionArn,
104 );
105 if (yield* hasAlchemyTags(id, tags)) return subscription;
106 }
107 return undefined;
108 });
109
110 const toAttrs = (subscription: mm.AddonSubscription) =>
111 subscription.AddonSubscriptionId !== undefined &&
112 subscription.AddonSubscriptionArn !== undefined &&
113 subscription.AddonName !== undefined
114 ? {
115 addonSubscriptionId: subscription.AddonSubscriptionId,
116 addonSubscriptionArn: subscription.AddonSubscriptionArn,
117 addonName: subscription.AddonName,
118 }
119 : undefined;
120
121 const observe = Effect.fn(function* (
122 id: string,
123 output: AddonSubscription["Attributes"] | undefined,
124 ) {
125 if (output?.addonSubscriptionId !== undefined) {
126 const found = yield* getById(output.addonSubscriptionId);
127 if (found !== undefined) {
128 return {
129 AddonSubscriptionId: output.addonSubscriptionId,
130 AddonSubscriptionArn: found.AddonSubscriptionArn,
131 AddonName: found.AddonName,
132 } satisfies mm.AddonSubscription;
133 }
134 }
135 return yield* findByAlchemyTags(id);
136 });
137
138 return AddonSubscription.Provider.of({
139 stables: ["addonSubscriptionId", "addonSubscriptionArn", "addonName"],
140
141 list: () =>
142 listAll.pipe(
143 Effect.map((subscriptions) =>
144 subscriptions.flatMap((s) => {
145 const attrs = toAttrs(s);
146 return attrs === undefined ? [] : [attrs];
147 }),
148 ),
149 ),
150
151 read: Effect.fn(function* ({ id, output }) {
152 const subscription = yield* observe(id, output);
153 if (subscription === undefined) return undefined;
154 const attrs = toAttrs(subscription);
155 if (attrs === undefined) return undefined;
156 const tags = yield* readMailManagerTags(attrs.addonSubscriptionArn);
157 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
158 }),
159
160 // The Add On product is create-only — changing it replaces the
161 // subscription.
162 diff: Effect.fn(function* ({ news, olds }) {
163 if (!isResolved(news)) return undefined;
164 if (olds !== undefined && olds.addonName !== news.addonName) {
165 return { action: "replace" } as const;
166 }
167 }),
168
169 reconcile: Effect.fn(function* ({ id, news, output, session }) {
170 const internalTags = yield* createInternalTags(id);
171 const desiredTags = { ...news.tags, ...internalTags };
172
173 // 1. OBSERVE — by cached id, falling back to an ownership-tag
174 // search.
175 let subscription = yield* observe(id, output);
176
177 // 2. ENSURE — create if missing; a Conflict (already subscribed)
178 // re-observes via the tag search.
179 if (subscription === undefined) {
180 yield* session.note(`subscribing to Add On ${news.addonName}`);
181 const created = yield* mm
182 .createAddonSubscription({
183 AddonName: news.addonName,
184 Tags: createTagsList(desiredTags),
185 })
186 .pipe(
187 Effect.catchTag("ConflictException", () =>
188 Effect.succeed(undefined),
189 ),
190 );
191 if (created !== undefined) {
192 const found = yield* getById(created.AddonSubscriptionId);
193 subscription = {
194 AddonSubscriptionId: created.AddonSubscriptionId,
195 AddonSubscriptionArn: found?.AddonSubscriptionArn,
196 AddonName: found?.AddonName ?? news.addonName,
197 };
198 } else {
199 subscription = yield* findByAlchemyTags(id);
200 }
201 }
202 const attrs =
203 subscription !== undefined ? toAttrs(subscription) : undefined;
204 if (attrs === undefined) {
205 return yield* Effect.fail(
206 new Error(
207 `Mail Manager Add On subscription for '${news.addonName}' not found after create`,
208 ),
209 );
210 }
211
212 // 3. SYNC — only tags are mutable.
213 yield* syncMailManagerTags(attrs.addonSubscriptionArn, desiredTags);
214
215 yield* session.note(attrs.addonSubscriptionId);
216 return attrs;
217 }),
218
219 delete: Effect.fn(function* ({ output }) {
220 // DeleteAddonSubscription is natively idempotent — deleting a
221 // nonexistent subscription succeeds (verified live).
222 yield* mm
223 .deleteAddonSubscription({
224 AddonSubscriptionId: output.addonSubscriptionId,
225 })
226 .pipe(Effect.asVoid);
227 }),
228 });
229 }),
230 );