| 1 | import * as AWS from "@/AWS"; |
| 2 | import * as Context from "effect/Context"; |
| 3 | import * as Duration from "effect/Duration"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Schedule from "effect/Schedule"; |
| 7 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 8 | import * 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). |
| 14 | export class ContactsBindingsFunction extends AWS.Lambda.Function<AWS.Lambda.Function>()( |
| 15 | "ContactsBindingsFunction", |
| 16 | ) {} |
| 17 | |
| 18 | export 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 | |
| 27 | export 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 | |
| 54 | export 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 | ); |