| 1 | import * as AWS from "@/AWS"; |
| 2 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Redacted from "effect/Redacted"; |
| 6 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 7 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 8 | import path from "pathe"; |
| 9 | |
| 10 | const main = path.resolve(import.meta.dirname, "platform-handler.ts"); |
| 11 | |
| 12 | /** |
| 13 | * Gated fixture for the SNS mobile-push bindings. SNS validates platform |
| 14 | * credentials at CreatePlatformApplication time (probe-verified: a fake GCM |
| 15 | * key fails with `InvalidParameterException` "Platform credentials are |
| 16 | * invalid"), so this fixture only deploys when real credentials are supplied: |
| 17 | * |
| 18 | * AWS_TEST_SNS_PLATFORM=1 |
| 19 | * AWS_TEST_SNS_PLATFORM_NAME (e.g. GCM) |
| 20 | * AWS_TEST_SNS_PLATFORM_CREDENTIAL |
| 21 | * AWS_TEST_SNS_PLATFORM_TOKEN (a registrable device token) |
| 22 | */ |
| 23 | export class PlatformFixture extends Context.Service< |
| 24 | PlatformFixture, |
| 25 | { application: AWS.SNS.PlatformApplication } |
| 26 | >()("PlatformFixture") {} |
| 27 | |
| 28 | export const PlatformFixtureLive = Layer.effect( |
| 29 | PlatformFixture, |
| 30 | Effect.gen(function* () { |
| 31 | const application = yield* AWS.SNS.PlatformApplication("TestPushApp", { |
| 32 | platform: process.env.AWS_TEST_SNS_PLATFORM_NAME ?? "GCM", |
| 33 | platformCredential: Redacted.make( |
| 34 | process.env.AWS_TEST_SNS_PLATFORM_CREDENTIAL ?? "", |
| 35 | ), |
| 36 | }); |
| 37 | return { application }; |
| 38 | }), |
| 39 | ); |
| 40 | |
| 41 | export class PlatformApiFunction extends AWS.Lambda.Function<AWS.Lambda.Function>()( |
| 42 | "PlatformApiFunction", |
| 43 | ) {} |
| 44 | |
| 45 | const formatError = (error: unknown) => |
| 46 | typeof error === "object" && error !== null && "_tag" in error |
| 47 | ? { ok: false as const, error: (error as { _tag: string })._tag } |
| 48 | : { ok: false as const, error: `${error}` }; |
| 49 | |
| 50 | export const PlatformApiFunctionLive = PlatformApiFunction.make( |
| 51 | { |
| 52 | main, |
| 53 | functionUrl: true, |
| 54 | }, |
| 55 | Effect.gen(function* () { |
| 56 | const { application } = yield* PlatformFixture; |
| 57 | |
| 58 | const createEndpoint = yield* AWS.SNS.CreatePlatformEndpoint(application); |
| 59 | const getEndpointAttributes = |
| 60 | yield* AWS.SNS.GetEndpointAttributes(application); |
| 61 | const setEndpointAttributes = |
| 62 | yield* AWS.SNS.SetEndpointAttributes(application); |
| 63 | const deleteEndpoint = yield* AWS.SNS.DeleteEndpoint(application); |
| 64 | const listEndpoints = |
| 65 | yield* AWS.SNS.ListEndpointsByPlatformApplication(application); |
| 66 | const publishToEndpoint = yield* AWS.SNS.PublishToEndpoint(application); |
| 67 | |
| 68 | return { |
| 69 | fetch: Effect.gen(function* () { |
| 70 | const request = yield* HttpServerRequest; |
| 71 | const url = new URL(request.originalUrl); |
| 72 | |
| 73 | if (request.method === "POST" && url.pathname === "/endpoint-cycle") { |
| 74 | const body = (yield* request.json) as { token: string }; |
| 75 | |
| 76 | // Create → read → disable → publish (tolerant; the device token |
| 77 | // may not be reachable) → list → delete. |
| 78 | const created = yield* createEndpoint({ Token: body.token }); |
| 79 | if (!created.EndpointArn) { |
| 80 | return yield* HttpServerResponse.json( |
| 81 | { ok: false, error: "no EndpointArn" }, |
| 82 | { status: 500 }, |
| 83 | ); |
| 84 | } |
| 85 | const attributes = yield* getEndpointAttributes({ |
| 86 | EndpointArn: created.EndpointArn, |
| 87 | }); |
| 88 | yield* setEndpointAttributes({ |
| 89 | EndpointArn: created.EndpointArn, |
| 90 | Attributes: { Enabled: "true" }, |
| 91 | }); |
| 92 | const published = yield* publishToEndpoint({ |
| 93 | TargetArn: created.EndpointArn, |
| 94 | Message: "alchemy sns platform binding test", |
| 95 | }).pipe(Effect.catch((error) => Effect.succeed(formatError(error)))); |
| 96 | const listed = yield* listEndpoints(); |
| 97 | yield* deleteEndpoint({ EndpointArn: created.EndpointArn }); |
| 98 | |
| 99 | return yield* HttpServerResponse.json({ |
| 100 | ok: true, |
| 101 | endpointArn: created.EndpointArn, |
| 102 | attributes: attributes.Attributes, |
| 103 | published, |
| 104 | endpointCount: (listed.Endpoints ?? []).length, |
| 105 | }); |
| 106 | } |
| 107 | |
| 108 | return yield* HttpServerResponse.json( |
| 109 | { error: "Not found" }, |
| 110 | { status: 404 }, |
| 111 | ); |
| 112 | }).pipe(Effect.orDie), |
| 113 | }; |
| 114 | }).pipe( |
| 115 | Effect.provide( |
| 116 | Layer.provideMerge( |
| 117 | PlatformFixtureLive, |
| 118 | Layer.mergeAll( |
| 119 | AWS.SNS.CreatePlatformEndpointHttp, |
| 120 | AWS.SNS.DeleteEndpointHttp, |
| 121 | AWS.SNS.GetEndpointAttributesHttp, |
| 122 | AWS.SNS.ListEndpointsByPlatformApplicationHttp, |
| 123 | AWS.SNS.PublishToEndpointHttp, |
| 124 | AWS.SNS.SetEndpointAttributesHttp, |
| 125 | ), |
| 126 | ), |
| 127 | ), |
| 128 | ), |
| 129 | ).pipe(Layer.provideMerge(PlatformFixtureLive)); |
| 130 | |
| 131 | export default PlatformApiFunctionLive; |