1import * as AWS from "@/AWS";
2import * as Context from "effect/Context";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Schedule from "effect/Schedule";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9
10// Bindings fixture: a personal contact with a deferred-activation email
11// channel and a daily rotation, plus a Lambda that exercises the SSM
12// Contacts runtime bindings against them. Requires the account's Incident
13// Manager replication set (gated behind AWS_TEST_INCIDENT_MANAGER).
14export class ContactsBindingsFunction extends AWS.Lambda.Function<AWS.Lambda.Function>()(
15 "ContactsBindingsFunction",
16) {}
17
18export class BoundContacts extends Context.Service<
19 BoundContacts,
20 {
21 oncall: AWS.SSMContacts.Contact;
22 email: AWS.SSMContacts.ContactChannel;
23 rotation: AWS.SSMContacts.Rotation;
24 }
25>()("BoundContacts") {}
26
27export const BoundContactsLive = Layer.effect(
28 BoundContacts,
29 Effect.gen(function* () {
30 const oncall = yield* AWS.SSMContacts.Contact("BindingsOncall", {
31 type: "PERSONAL",
32 displayName: "Bindings Fixture On-Call",
33 });
34 const email = yield* AWS.SSMContacts.ContactChannel("BindingsEmail", {
35 contactId: oncall.contactArn,
36 type: "EMAIL",
37 deliveryAddress: { SimpleAddress: "oncall@example.com" },
38 deferActivation: true,
39 });
40 const rotation = yield* AWS.SSMContacts.Rotation("BindingsRotation", {
41 contactIds: [oncall.contactArn],
42 timeZoneId: "America/Los_Angeles",
43 startTime: "2030-01-01T00:00:00Z",
44 recurrence: {
45 NumberOfOnCalls: 1,
46 RecurrenceMultiplier: 1,
47 DailySettings: [{ HourOfDay: 9, MinuteOfHour: 0 }],
48 },
49 });
50 return { oncall, email, rotation };
51 }),
52);
53
54export default ContactsBindingsFunction.make(
55 {
56 main: import.meta.url,
57 functionUrl: true,
58 timeout: Duration.seconds(60),
59 },
60 Effect.gen(function* () {
61 const { oncall, email, rotation } = yield* BoundContacts;
62
63 const startEngagement = yield* AWS.SSMContacts.StartEngagement(oncall);
64 const listPagesByContact =
65 yield* AWS.SSMContacts.ListPagesByContact(oncall);
66 const stopEngagement = yield* AWS.SSMContacts.StopEngagement();
67 const describeEngagement = yield* AWS.SSMContacts.DescribeEngagement();
68 const listEngagements = yield* AWS.SSMContacts.ListEngagements();
69 const acceptPage = yield* AWS.SSMContacts.AcceptPage();
70 const describePage = yield* AWS.SSMContacts.DescribePage();
71 const listPageReceipts = yield* AWS.SSMContacts.ListPageReceipts();
72 const listPageResolutions = yield* AWS.SSMContacts.ListPageResolutions();
73 const listPagesByEngagement =
74 yield* AWS.SSMContacts.ListPagesByEngagement();
75 const sendActivationCode = yield* AWS.SSMContacts.SendActivationCode(email);
76 const activateContactChannel =
77 yield* AWS.SSMContacts.ActivateContactChannel(email);
78 const deactivateContactChannel =
79 yield* AWS.SSMContacts.DeactivateContactChannel(email);
80 const listRotationShifts =
81 yield* AWS.SSMContacts.ListRotationShifts(rotation);
82 const listPreviewRotationShifts =
83 yield* AWS.SSMContacts.ListPreviewRotationShifts();
84 const createRotationOverride =
85 yield* AWS.SSMContacts.CreateRotationOverride(rotation);
86 const getRotationOverride =
87 yield* AWS.SSMContacts.GetRotationOverride(rotation);
88 const deleteRotationOverride =
89 yield* AWS.SSMContacts.DeleteRotationOverride(rotation);
90 const listRotationOverrides =
91 yield* AWS.SSMContacts.ListRotationOverrides(rotation);
92
93 const bound = {
94 startEngagement,
95 listPagesByContact,
96 stopEngagement,
97 describeEngagement,
98 listEngagements,
99 acceptPage,
100 describePage,
101 listPageReceipts,
102 listPageResolutions,
103 listPagesByEngagement,
104 sendActivationCode,
105 activateContactChannel,
106 deactivateContactChannel,
107 listRotationShifts,
108 listPreviewRotationShifts,
109 createRotationOverride,
110 getRotationOverride,
111 deleteRotationOverride,
112 listRotationOverrides,
113 };
114
115 // Accessor for the contact's ARN — an Accessor<string> at init scope,
116 // resolved with a second yield inside the runtime fetch handler.
117 const oncallArn = yield* oncall.contactArn;
118
119 return {
120 fetch: Effect.gen(function* () {
121 const request = yield* HttpServerRequest;
122 const url = new URL(request.originalUrl);
123 const pathname = url.pathname;
124
125 if (request.method === "GET" && pathname === "/bindings") {
126 return yield* HttpServerResponse.json({
127 bound: Object.keys(bound),
128 });
129 }
130
131 if (request.method === "GET" && pathname === "/shifts") {
132 const week = 7 * 24 * 60 * 60 * 1000;
133 const { RotationShifts } = yield* listRotationShifts({
134 EndTime: new Date(Date.now() + week),
135 });
136 const preview = yield* listPreviewRotationShifts({
137 EndTime: new Date(Date.now() + week),
138 Members: [yield* oncallArn],
139 TimeZoneId: "America/Los_Angeles",
140 Recurrence: {
141 NumberOfOnCalls: 1,
142 RecurrenceMultiplier: 1,
143 DailySettings: [{ HourOfDay: 9, MinuteOfHour: 0 }],
144 },
145 });
146 return yield* HttpServerResponse.json({
147 shifts: (RotationShifts ?? []).length,
148 previewShifts: (preview.RotationShifts ?? []).length,
149 });
150 }
151
152 if (request.method === "GET" && pathname === "/override") {
153 const hour = 60 * 60 * 1000;
154 const { RotationOverrideId } = yield* createRotationOverride({
155 NewContactIds: [yield* oncallArn],
156 StartTime: new Date(Date.now() + hour),
157 EndTime: new Date(Date.now() + 2 * hour),
158 });
159 const override = yield* getRotationOverride({ RotationOverrideId });
160 const { RotationOverrides } = yield* listRotationOverrides({
161 StartTime: new Date(),
162 EndTime: new Date(Date.now() + 3 * hour),
163 });
164 yield* deleteRotationOverride({ RotationOverrideId });
165 return yield* HttpServerResponse.json({
166 overrideId: RotationOverrideId,
167 newContactIds: override.NewContactIds,
168 listed: (RotationOverrides ?? []).length,
169 });
170 }
171
172 if (request.method === "GET" && pathname === "/engage") {
173 const { EngagementArn } = yield* startEngagement({
174 Sender: "alchemy-bindings-fixture",
175 Subject: "alchemy bindings fixture engagement",
176 Content: "Exercising the SSM Contacts runtime bindings.",
177 });
178 const engagement = yield* describeEngagement({
179 EngagementId: EngagementArn,
180 });
181 // Pages materialize asynchronously — poll briefly (bounded).
182 const pages = yield* listPagesByEngagement({
183 EngagementId: EngagementArn,
184 }).pipe(
185 Effect.map((r) => r.Pages ?? []),
186 Effect.repeat({
187 schedule: Schedule.spaced("2 seconds"),
188 until: (pages): boolean => pages.length > 0,
189 times: 10,
190 }),
191 );
192 let receipts = 0;
193 let resolutions = 0;
194 const pageArn = pages[0]?.PageArn;
195 if (pageArn !== undefined) {
196 yield* describePage({ PageId: pageArn });
197 receipts = yield* listPageReceipts({ PageId: pageArn }).pipe(
198 Effect.map((r) => (r.Receipts ?? []).length),
199 );
200 resolutions = yield* listPageResolutions({ PageId: pageArn }).pipe(
201 Effect.map((r) => (r.PageResolutions ?? []).length),
202 );
203 }
204 yield* stopEngagement({
205 EngagementId: EngagementArn,
206 Reason: "fixture done",
207 });
208 return yield* HttpServerResponse.json({
209 engagementArn: EngagementArn,
210 subject: engagement.Subject,
211 pages: pages.length,
212 receipts,
213 resolutions,
214 });
215 }
216
217 if (request.method === "GET" && pathname === "/pages") {
218 const { Engagements } = yield* listEngagements();
219 const { Pages } = yield* listPagesByContact();
220 return yield* HttpServerResponse.json({
221 engagements: (Engagements ?? []).length,
222 pages: (Pages ?? []).length,
223 });
224 }
225
226 if (request.method === "GET" && pathname === "/channel") {
227 yield* sendActivationCode();
228 yield* deactivateContactChannel();
229 return yield* HttpServerResponse.json({ ok: true });
230 }
231
232 return yield* HttpServerResponse.json(
233 { error: "Not found", method: request.method, pathname },
234 { status: 404 },
235 );
236 }).pipe(Effect.orDie),
237 };
238 }).pipe(
239 Effect.provide(
240 Layer.mergeAll(
241 AWS.SSMContacts.StartEngagementHttp,
242 AWS.SSMContacts.ListPagesByContactHttp,
243 AWS.SSMContacts.StopEngagementHttp,
244 AWS.SSMContacts.DescribeEngagementHttp,
245 AWS.SSMContacts.ListEngagementsHttp,
246 AWS.SSMContacts.AcceptPageHttp,
247 AWS.SSMContacts.DescribePageHttp,
248 AWS.SSMContacts.ListPageReceiptsHttp,
249 AWS.SSMContacts.ListPageResolutionsHttp,
250 AWS.SSMContacts.ListPagesByEngagementHttp,
251 AWS.SSMContacts.SendActivationCodeHttp,
252 AWS.SSMContacts.ActivateContactChannelHttp,
253 AWS.SSMContacts.DeactivateContactChannelHttp,
254 AWS.SSMContacts.ListRotationShiftsHttp,
255 AWS.SSMContacts.ListPreviewRotationShiftsHttp,
256 AWS.SSMContacts.CreateRotationOverrideHttp,
257 AWS.SSMContacts.GetRotationOverrideHttp,
258 AWS.SSMContacts.DeleteRotationOverrideHttp,
259 AWS.SSMContacts.ListRotationOverridesHttp,
260 BoundContactsLive,
261 ),
262 ),
263 ),
264);