@shware/workflow 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +62 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +62 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +176 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +75 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +75 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +172 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +37 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +37 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.mjs +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +82 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +41 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +41 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +81 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +28 -9
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +28 -9
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +88 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +44 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +44 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +86 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +63 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +63 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +143 -2
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +143 -2
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +26 -0
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +26 -0
- package/dist/store/tables.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"senders.cjs","names":["isUnsubscribeLink"],"sources":["../../src/cloudflare/senders.ts"],"sourcesContent":["import { type ResolvedEmail, isUnsubscribeLink } from '../email';\nimport type { MessageSender, MessageTracking, OutboundMessage } from '../engine';\nimport type { ScalarIR } from '../ir';\nimport type { EmailBindingLike } from './bindings';\n\nexport type { EmailAddress } from '../email';\nexport type { EmailBindingLike } from './bindings';\n\n/** A rendered email: the template's resolved metadata (`resolveEmail`), its subject and its HTML. */\nexport interface RenderedEmail extends ResolvedEmail {\n subject: string;\n html: string;\n}\n\n/**\n * Template renderer, injected by the app — this package's core stays free of\n * any react-email dependency (`@shware/workflow/react-email` ships one over a\n * registry). The subject comes back final: it is sent exactly as written, from\n * the template's own sender, and the unsubscribe link is the one the HTML was\n * rendered with.\n */\nexport type EmailRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<RenderedEmail>;\n\nexport interface CfEmailOptions {\n /** The Worker's `send_email` binding. */\n binding: EmailBindingLike;\n render: EmailRenderer;\n}\n\n/**\n * Send straight through Cloudflare Email Service — a binding call, no outbound\n * HTTP. Throwing hands retries to the CF step, and the message's\n * idempotencyKey travels with the call so the binding can drop the duplicate a\n * retry or replay produces.\n */\nexport class CfEmailSender implements MessageSender {\n constructor(private readonly options: CfEmailOptions) {}\n\n async send(message: OutboundMessage): Promise<void> {\n if (message.channel !== 'email') {\n throw new Error(`CfEmailSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`CfEmailSender: no recipient for user '${message.userId}'`);\n }\n const { subject, html, from, replyTo, unsubscribeUrl } = await this.options.render(\n message.template,\n message.props\n );\n const tracked =\n message.tracking === undefined ? html : trackEmail(html, message.tracking, unsubscribeUrl);\n // The tracked links resolve before the email exists anywhere a scanner could follow them\n await message.tracking?.commit?.();\n await this.options.binding.send({\n from,\n to: message.recipient,\n ...(replyTo === undefined ? {} : { replyTo }),\n subject,\n html: tracked,\n idempotencyKey: message.idempotencyKey,\n });\n }\n}\n\nconst LINK = /(<a\\b[^>]*?\\shref\\s*=\\s*)([\"'])(https?:\\/\\/[^\"']+)\\2/gi;\n\n/**\n * An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through\n * `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —\n * as the unsubscribe link when it is the one the email was rendered with, so what a click on it\n * means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the\n * end when there is none.\n */\nexport function trackEmail(\n html: string,\n tracking: MessageTracking,\n unsubscribeUrl?: string\n): string {\n const { link, pixel } = tracking;\n let out = html;\n if (link !== undefined) {\n out = out.replace(LINK, (_, start: string, quote: string, attribute: string) => {\n const url = unescapeAttribute(attribute);\n const purpose = isUnsubscribeLink(url, unsubscribeUrl) ? 'unsubscribe' : 'link';\n return `${start}${quote}${escapeAttribute(link(url, purpose))}${quote}`;\n });\n }\n if (pixel !== undefined) {\n const image = `<img src=\"${escapeAttribute(pixel)}\" width=\"1\" height=\"1\" alt=\"\" style=\"display:block;border:0;width:1px;height:1px\" />`;\n out = /<\\/body>/i.test(out) ? out.replace(/<\\/body>/i, `${image}</body>`) : `${out}${image}`;\n }\n return out;\n}\n\nfunction unescapeAttribute(value: string): string {\n return value.replaceAll('&', '&').replaceAll('"', '\"').replaceAll(''', \"'\");\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replaceAll('&', '&').replaceAll('\"', '"');\n}\n\n/** Development sender: structured logs, for eyeballing the pipeline. */\nexport class LogMessageSender implements MessageSender {\n async send(message: OutboundMessage): Promise<void> {\n const { tracking: _tracking, ...logged } = message;\n console.log(`[send] ${JSON.stringify(logged)}`);\n }\n}\n\n/**\n * Webhook sender: POST to an external delivery service (a Resend/SES gateway,\n * say). The idempotency key travels with it, so the receiver de-duplicates\n * retries and replays on idempotencyKey.\n */\nexport class WebhookMessageSender implements MessageSender {\n constructor(private readonly url: string) {}\n\n async send(message: OutboundMessage): Promise<void> {\n const response = await fetch(this.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'idempotency-key': message.idempotencyKey,\n },\n body: JSON.stringify(message),\n });\n if (!response.ok) {\n throw new Error(`message webhook failed: ${response.status}`);\n }\n }\n}\n\n/**\n * One sender per channel: the runner has a single outlet, this fans it out.\n * A channel without a sender fails the send (a misconfiguration, not a retry).\n */\nexport function routeByChannel(\n senders: Partial<Record<OutboundMessage['channel'], MessageSender>>\n): MessageSender {\n return {\n async send(message) {\n const sender = senders[message.channel];\n if (sender === undefined) {\n throw new Error(`no sender configured for channel '${message.channel}'`);\n }\n return sender.send(message);\n },\n };\n}\n\n/**\n * One push outlet per platform, chosen by the device each copy goes to (the\n * engine sends one message per device, and the host's directory says which\n * platform a token belongs to): `ios` straight to APNs, `android` through FCM,\n * say. A platform with no sender fails the step rather than guessing.\n */\nexport function routeByPlatform(senders: Record<string, MessageSender>): MessageSender {\n return {\n async send(message) {\n const platform = message.device?.platform;\n if (platform === undefined) {\n throw new Error(`push for user '${message.userId}' carries no device`);\n }\n if (!Object.hasOwn(senders, platform)) {\n throw new Error(`no push sender configured for platform '${platform}'`);\n }\n return senders[platform].send(message);\n },\n };\n}\n"],"mappings":";;;;;;;;;AAsCA,IAAa,gBAAb,MAAoD;CACrB;CAA7B,YAAY,SAA0C;EAAzB,KAAA,UAAA;CAA0B;CAEvD,MAAM,KAAK,SAAyC;EAClD,IAAI,QAAQ,YAAY,SACtB,MAAM,IAAI,MAAM,uCAAuC,QAAQ,QAAQ,EAAE;EAE3E,IAAI,QAAQ,cAAc,KAAA,GACxB,MAAM,IAAI,MAAM,yCAAyC,QAAQ,OAAO,EAAE;EAE5E,MAAM,EAAE,SAAS,MAAM,MAAM,SAAS,mBAAmB,MAAM,KAAK,QAAQ,OAC1E,QAAQ,UACR,QAAQ,KACV;EACA,MAAM,UACJ,QAAQ,aAAa,KAAA,IAAY,OAAO,WAAW,MAAM,QAAQ,UAAU,cAAc;EAE3F,MAAM,QAAQ,UAAU,SAAS;EACjC,MAAM,KAAK,QAAQ,QAAQ,KAAK;GAC9B;GACA,IAAI,QAAQ;GACZ,GAAI,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ;GAC3C;GACA,MAAM;GACN,gBAAgB,QAAQ;EAC1B,CAAC;CACH;AACF;AAEA,MAAM,OAAO;;;;;;;;AASb,SAAgB,WACd,MACA,UACA,gBACQ;CACR,MAAM,EAAE,MAAM,UAAU;CACxB,IAAI,MAAM;CACV,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,QAAQ,OAAO,GAAG,OAAe,OAAe,cAAsB;EAC9E,MAAM,MAAM,kBAAkB,SAAS;EACvC,MAAM,UAAUA,cAAAA,kBAAkB,KAAK,cAAc,IAAI,gBAAgB;EACzE,OAAO,GAAG,QAAQ,QAAQ,gBAAgB,KAAK,KAAK,OAAO,CAAC,IAAI;CAClE,CAAC;CAEH,IAAI,UAAU,KAAA,GAAW;EACvB,MAAM,QAAQ,aAAa,gBAAgB,KAAK,EAAE;EAClD,MAAM,YAAY,KAAK,GAAG,IAAI,IAAI,QAAQ,aAAa,GAAG,MAAM,QAAQ,IAAI,GAAG,MAAM;CACvF;CACA,OAAO;AACT;AAEA,SAAS,kBAAkB,OAAuB;CAChD,OAAO,MAAM,WAAW,SAAS,GAAG,CAAC,CAAC,WAAW,UAAU,IAAG,CAAC,CAAC,WAAW,UAAU,GAAG;AAC1F;AAEA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,MAAM,WAAW,KAAK,OAAO,CAAC,CAAC,WAAW,MAAK,QAAQ;AAChE;;AAGA,IAAa,mBAAb,MAAuD;CACrD,MAAM,KAAK,SAAyC;EAClD,MAAM,EAAE,UAAU,WAAW,GAAG,WAAW;EAC3C,QAAQ,IAAI,UAAU,KAAK,UAAU,MAAM,GAAG;CAChD;AACF;;;;;;AAOA,IAAa,uBAAb,MAA2D;CAC5B;CAA7B,YAAY,KAA8B;EAAb,KAAA,MAAA;CAAc;CAE3C,MAAM,KAAK,SAAyC;EAClD,MAAM,WAAW,MAAM,MAAM,KAAK,KAAK;GACrC,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,mBAAmB,QAAQ;GAC7B;GACA,MAAM,KAAK,UAAU,OAAO;EAC9B,CAAC;EACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,2BAA2B,SAAS,QAAQ;CAEhE;AACF;;;;;AAMA,SAAgB,eACd,SACe;CACf,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,MAAM,SAAS,QAAQ,QAAQ;EAC/B,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,MAAM,qCAAqC,QAAQ,QAAQ,EAAE;EAEzE,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;;;;;;;AAQA,SAAgB,gBAAgB,SAAuD;CACrF,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,MAAM,WAAW,QAAQ,QAAQ;EACjC,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MAAM,kBAAkB,QAAQ,OAAO,oBAAoB;EAEvE,IAAI,CAAC,OAAO,OAAO,SAAS,QAAQ,GAClC,MAAM,IAAI,MAAM,2CAA2C,SAAS,EAAE;EAExE,OAAO,QAAQ,SAAS,CAAC,KAAK,OAAO;CACvC,EACF;AACF"}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { ScalarIR } from "../ir.cjs";
|
|
2
|
-
import { MessageSender, MessageTracking, OutboundMessage } from "../engine/ports.cjs";
|
|
3
|
-
import "../engine/index.cjs";
|
|
4
|
-
import { EmailAddress, ResolvedEmail } from "../email.cjs";
|
|
5
|
-
import { EmailBindingLike } from "./bindings.cjs";
|
|
6
|
-
//#region src/cloudflare/senders.d.ts
|
|
7
|
-
/** A rendered email: the template's resolved metadata (`resolveEmail`), its subject and its HTML. */
|
|
8
|
-
export interface RenderedEmail extends ResolvedEmail {
|
|
9
|
-
subject: string;
|
|
10
|
-
html: string;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Template renderer, injected by the app — this package's core stays free of
|
|
14
|
-
* any react-email dependency (`@shware/workflow/react-email` ships one over a
|
|
15
|
-
* registry). The subject comes back final: it is sent exactly as written, from
|
|
16
|
-
* the template's own sender, and the unsubscribe link is the one the HTML was
|
|
17
|
-
* rendered with.
|
|
18
|
-
*/
|
|
19
|
-
export type EmailRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<RenderedEmail>;
|
|
20
|
-
export interface CfEmailOptions {
|
|
21
|
-
/** The Worker's `send_email` binding. */
|
|
22
|
-
binding: EmailBindingLike;
|
|
23
|
-
render: EmailRenderer;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Send straight through Cloudflare Email Service — a binding call, no outbound
|
|
27
|
-
* HTTP. Throwing hands retries to the CF step, and the message's
|
|
28
|
-
* idempotencyKey travels with the call so the binding can drop the duplicate a
|
|
29
|
-
* retry or replay produces.
|
|
30
|
-
*/
|
|
31
|
-
export declare class CfEmailSender implements MessageSender {
|
|
32
|
-
private readonly options;
|
|
33
|
-
constructor(options: CfEmailOptions);
|
|
34
|
-
send(message: OutboundMessage): Promise<void>;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
38
|
-
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —
|
|
39
|
-
* as the unsubscribe link when it is the one the email was rendered with, so what a click on it
|
|
40
|
-
* means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the
|
|
41
|
-
* end when there is none.
|
|
42
|
-
*/
|
|
43
|
-
export declare function trackEmail(html: string, tracking: MessageTracking, unsubscribeUrl?: string): string;
|
|
44
|
-
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
45
|
-
export declare class LogMessageSender implements MessageSender {
|
|
46
|
-
send(message: OutboundMessage): Promise<void>;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
50
|
-
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
51
|
-
* retries and replays on idempotencyKey.
|
|
52
|
-
*/
|
|
53
|
-
export declare class WebhookMessageSender implements MessageSender {
|
|
54
|
-
private readonly url;
|
|
55
|
-
constructor(url: string);
|
|
56
|
-
send(message: OutboundMessage): Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
60
|
-
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
61
|
-
*/
|
|
62
|
-
export declare function routeByChannel(senders: Partial<Record<OutboundMessage['channel'], MessageSender>>): MessageSender;
|
|
63
|
-
/**
|
|
64
|
-
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
65
|
-
* engine sends one message per device, and the host's directory says which
|
|
66
|
-
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
67
|
-
* say. A platform with no sender fails the step rather than guessing.
|
|
68
|
-
*/
|
|
69
|
-
export declare function routeByPlatform(senders: Record<string, MessageSender>): MessageSender;
|
|
70
|
-
//#endregion
|
|
71
|
-
export type { EmailAddress, EmailBindingLike };
|
|
72
|
-
//# sourceMappingURL=senders.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"senders.d.cts","names":[],"sources":["../../src/cloudflare/senders.ts"],"mappings":";;;;;;;iBASiB,sBAAsB;EACrC;EACA;;;;;;;;;YAUU,iBACV,kBACA,OAAO,eAAe,0BACnB,QAAQ;iBAEI;;EAEf,SAAS;EACT,QAAQ;;;;;;;;qBASG,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB;;;;;;;;;wBAmCxB,WACd,cACA,UAAU,iBACV;;qBA2BW,4BAA4B;EACjC,KAAK,SAAS,kBAAkB;;;;;;;qBAW3B,gCAAgC;mBACd;EAA7B,YAA6B;EAEvB,KAAK,SAAS,kBAAkB;;;;;;wBAmBxB,eACd,SAAS,QAAQ,OAAO,4BAA4B,kBACnD;;;;;;;wBAkBa,gBAAgB,SAAS,eAAe,iBAAiB"}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { ScalarIR } from "../ir.mjs";
|
|
2
|
-
import { MessageSender, MessageTracking, OutboundMessage } from "../engine/ports.mjs";
|
|
3
|
-
import "../engine/index.mjs";
|
|
4
|
-
import { EmailAddress, ResolvedEmail } from "../email.mjs";
|
|
5
|
-
import { EmailBindingLike } from "./bindings.mjs";
|
|
6
|
-
//#region src/cloudflare/senders.d.ts
|
|
7
|
-
/** A rendered email: the template's resolved metadata (`resolveEmail`), its subject and its HTML. */
|
|
8
|
-
export interface RenderedEmail extends ResolvedEmail {
|
|
9
|
-
subject: string;
|
|
10
|
-
html: string;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Template renderer, injected by the app — this package's core stays free of
|
|
14
|
-
* any react-email dependency (`@shware/workflow/react-email` ships one over a
|
|
15
|
-
* registry). The subject comes back final: it is sent exactly as written, from
|
|
16
|
-
* the template's own sender, and the unsubscribe link is the one the HTML was
|
|
17
|
-
* rendered with.
|
|
18
|
-
*/
|
|
19
|
-
export type EmailRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<RenderedEmail>;
|
|
20
|
-
export interface CfEmailOptions {
|
|
21
|
-
/** The Worker's `send_email` binding. */
|
|
22
|
-
binding: EmailBindingLike;
|
|
23
|
-
render: EmailRenderer;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Send straight through Cloudflare Email Service — a binding call, no outbound
|
|
27
|
-
* HTTP. Throwing hands retries to the CF step, and the message's
|
|
28
|
-
* idempotencyKey travels with the call so the binding can drop the duplicate a
|
|
29
|
-
* retry or replay produces.
|
|
30
|
-
*/
|
|
31
|
-
export declare class CfEmailSender implements MessageSender {
|
|
32
|
-
private readonly options;
|
|
33
|
-
constructor(options: CfEmailOptions);
|
|
34
|
-
send(message: OutboundMessage): Promise<void>;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
38
|
-
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —
|
|
39
|
-
* as the unsubscribe link when it is the one the email was rendered with, so what a click on it
|
|
40
|
-
* means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the
|
|
41
|
-
* end when there is none.
|
|
42
|
-
*/
|
|
43
|
-
export declare function trackEmail(html: string, tracking: MessageTracking, unsubscribeUrl?: string): string;
|
|
44
|
-
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
45
|
-
export declare class LogMessageSender implements MessageSender {
|
|
46
|
-
send(message: OutboundMessage): Promise<void>;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
50
|
-
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
51
|
-
* retries and replays on idempotencyKey.
|
|
52
|
-
*/
|
|
53
|
-
export declare class WebhookMessageSender implements MessageSender {
|
|
54
|
-
private readonly url;
|
|
55
|
-
constructor(url: string);
|
|
56
|
-
send(message: OutboundMessage): Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
60
|
-
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
61
|
-
*/
|
|
62
|
-
export declare function routeByChannel(senders: Partial<Record<OutboundMessage['channel'], MessageSender>>): MessageSender;
|
|
63
|
-
/**
|
|
64
|
-
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
65
|
-
* engine sends one message per device, and the host's directory says which
|
|
66
|
-
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
67
|
-
* say. A platform with no sender fails the step rather than guessing.
|
|
68
|
-
*/
|
|
69
|
-
export declare function routeByPlatform(senders: Record<string, MessageSender>): MessageSender;
|
|
70
|
-
//#endregion
|
|
71
|
-
export type { EmailAddress, EmailBindingLike };
|
|
72
|
-
//# sourceMappingURL=senders.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"senders.d.mts","names":[],"sources":["../../src/cloudflare/senders.ts"],"mappings":";;;;;;;iBASiB,sBAAsB;EACrC;EACA;;;;;;;;;YAUU,iBACV,kBACA,OAAO,eAAe,0BACnB,QAAQ;iBAEI;;EAEf,SAAS;EACT,QAAQ;;;;;;;;qBASG,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB;;;;;;;;;wBAmCxB,WACd,cACA,UAAU,iBACV;;qBA2BW,4BAA4B;EACjC,KAAK,SAAS,kBAAkB;;;;;;;qBAW3B,gCAAgC;mBACd;EAA7B,YAA6B;EAEvB,KAAK,SAAS,kBAAkB;;;;;;wBAmBxB,eACd,SAAS,QAAQ,OAAO,4BAA4B,kBACnD;;;;;;;wBAkBa,gBAAgB,SAAS,eAAe,iBAAiB"}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { isUnsubscribeLink } from "../email.mjs";
|
|
2
|
-
//#region src/cloudflare/senders.ts
|
|
3
|
-
/**
|
|
4
|
-
* Send straight through Cloudflare Email Service — a binding call, no outbound
|
|
5
|
-
* HTTP. Throwing hands retries to the CF step, and the message's
|
|
6
|
-
* idempotencyKey travels with the call so the binding can drop the duplicate a
|
|
7
|
-
* retry or replay produces.
|
|
8
|
-
*/
|
|
9
|
-
var CfEmailSender = class {
|
|
10
|
-
options;
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.options = options;
|
|
13
|
-
}
|
|
14
|
-
async send(message) {
|
|
15
|
-
if (message.channel !== "email") throw new Error(`CfEmailSender: unsupported channel '${message.channel}'`);
|
|
16
|
-
if (message.recipient === void 0) throw new Error(`CfEmailSender: no recipient for user '${message.userId}'`);
|
|
17
|
-
const { subject, html, from, replyTo, unsubscribeUrl } = await this.options.render(message.template, message.props);
|
|
18
|
-
const tracked = message.tracking === void 0 ? html : trackEmail(html, message.tracking, unsubscribeUrl);
|
|
19
|
-
await message.tracking?.commit?.();
|
|
20
|
-
await this.options.binding.send({
|
|
21
|
-
from,
|
|
22
|
-
to: message.recipient,
|
|
23
|
-
...replyTo === void 0 ? {} : { replyTo },
|
|
24
|
-
subject,
|
|
25
|
-
html: tracked,
|
|
26
|
-
idempotencyKey: message.idempotencyKey
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
const LINK = /(<a\b[^>]*?\shref\s*=\s*)(["'])(https?:\/\/[^"']+)\2/gi;
|
|
31
|
-
/**
|
|
32
|
-
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
33
|
-
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —
|
|
34
|
-
* as the unsubscribe link when it is the one the email was rendered with, so what a click on it
|
|
35
|
-
* means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the
|
|
36
|
-
* end when there is none.
|
|
37
|
-
*/
|
|
38
|
-
function trackEmail(html, tracking, unsubscribeUrl) {
|
|
39
|
-
const { link, pixel } = tracking;
|
|
40
|
-
let out = html;
|
|
41
|
-
if (link !== void 0) out = out.replace(LINK, (_, start, quote, attribute) => {
|
|
42
|
-
const url = unescapeAttribute(attribute);
|
|
43
|
-
const purpose = isUnsubscribeLink(url, unsubscribeUrl) ? "unsubscribe" : "link";
|
|
44
|
-
return `${start}${quote}${escapeAttribute(link(url, purpose))}${quote}`;
|
|
45
|
-
});
|
|
46
|
-
if (pixel !== void 0) {
|
|
47
|
-
const image = `<img src="${escapeAttribute(pixel)}" width="1" height="1" alt="" style="display:block;border:0;width:1px;height:1px" />`;
|
|
48
|
-
out = /<\/body>/i.test(out) ? out.replace(/<\/body>/i, `${image}</body>`) : `${out}${image}`;
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
function unescapeAttribute(value) {
|
|
53
|
-
return value.replaceAll("&", "&").replaceAll(""", "\"").replaceAll("'", "'");
|
|
54
|
-
}
|
|
55
|
-
function escapeAttribute(value) {
|
|
56
|
-
return value.replaceAll("&", "&").replaceAll("\"", """);
|
|
57
|
-
}
|
|
58
|
-
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
59
|
-
var LogMessageSender = class {
|
|
60
|
-
async send(message) {
|
|
61
|
-
const { tracking: _tracking, ...logged } = message;
|
|
62
|
-
console.log(`[send] ${JSON.stringify(logged)}`);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
67
|
-
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
68
|
-
* retries and replays on idempotencyKey.
|
|
69
|
-
*/
|
|
70
|
-
var WebhookMessageSender = class {
|
|
71
|
-
url;
|
|
72
|
-
constructor(url) {
|
|
73
|
-
this.url = url;
|
|
74
|
-
}
|
|
75
|
-
async send(message) {
|
|
76
|
-
const response = await fetch(this.url, {
|
|
77
|
-
method: "POST",
|
|
78
|
-
headers: {
|
|
79
|
-
"content-type": "application/json",
|
|
80
|
-
"idempotency-key": message.idempotencyKey
|
|
81
|
-
},
|
|
82
|
-
body: JSON.stringify(message)
|
|
83
|
-
});
|
|
84
|
-
if (!response.ok) throw new Error(`message webhook failed: ${response.status}`);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
89
|
-
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
90
|
-
*/
|
|
91
|
-
function routeByChannel(senders) {
|
|
92
|
-
return { async send(message) {
|
|
93
|
-
const sender = senders[message.channel];
|
|
94
|
-
if (sender === void 0) throw new Error(`no sender configured for channel '${message.channel}'`);
|
|
95
|
-
return sender.send(message);
|
|
96
|
-
} };
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
100
|
-
* engine sends one message per device, and the host's directory says which
|
|
101
|
-
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
102
|
-
* say. A platform with no sender fails the step rather than guessing.
|
|
103
|
-
*/
|
|
104
|
-
function routeByPlatform(senders) {
|
|
105
|
-
return { async send(message) {
|
|
106
|
-
const platform = message.device?.platform;
|
|
107
|
-
if (platform === void 0) throw new Error(`push for user '${message.userId}' carries no device`);
|
|
108
|
-
if (!Object.hasOwn(senders, platform)) throw new Error(`no push sender configured for platform '${platform}'`);
|
|
109
|
-
return senders[platform].send(message);
|
|
110
|
-
} };
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
export { CfEmailSender, LogMessageSender, WebhookMessageSender, routeByChannel, routeByPlatform, trackEmail };
|
|
114
|
-
|
|
115
|
-
//# sourceMappingURL=senders.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"senders.mjs","names":[],"sources":["../../src/cloudflare/senders.ts"],"sourcesContent":["import { type ResolvedEmail, isUnsubscribeLink } from '../email';\nimport type { MessageSender, MessageTracking, OutboundMessage } from '../engine';\nimport type { ScalarIR } from '../ir';\nimport type { EmailBindingLike } from './bindings';\n\nexport type { EmailAddress } from '../email';\nexport type { EmailBindingLike } from './bindings';\n\n/** A rendered email: the template's resolved metadata (`resolveEmail`), its subject and its HTML. */\nexport interface RenderedEmail extends ResolvedEmail {\n subject: string;\n html: string;\n}\n\n/**\n * Template renderer, injected by the app — this package's core stays free of\n * any react-email dependency (`@shware/workflow/react-email` ships one over a\n * registry). The subject comes back final: it is sent exactly as written, from\n * the template's own sender, and the unsubscribe link is the one the HTML was\n * rendered with.\n */\nexport type EmailRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<RenderedEmail>;\n\nexport interface CfEmailOptions {\n /** The Worker's `send_email` binding. */\n binding: EmailBindingLike;\n render: EmailRenderer;\n}\n\n/**\n * Send straight through Cloudflare Email Service — a binding call, no outbound\n * HTTP. Throwing hands retries to the CF step, and the message's\n * idempotencyKey travels with the call so the binding can drop the duplicate a\n * retry or replay produces.\n */\nexport class CfEmailSender implements MessageSender {\n constructor(private readonly options: CfEmailOptions) {}\n\n async send(message: OutboundMessage): Promise<void> {\n if (message.channel !== 'email') {\n throw new Error(`CfEmailSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`CfEmailSender: no recipient for user '${message.userId}'`);\n }\n const { subject, html, from, replyTo, unsubscribeUrl } = await this.options.render(\n message.template,\n message.props\n );\n const tracked =\n message.tracking === undefined ? html : trackEmail(html, message.tracking, unsubscribeUrl);\n // The tracked links resolve before the email exists anywhere a scanner could follow them\n await message.tracking?.commit?.();\n await this.options.binding.send({\n from,\n to: message.recipient,\n ...(replyTo === undefined ? {} : { replyTo }),\n subject,\n html: tracked,\n idempotencyKey: message.idempotencyKey,\n });\n }\n}\n\nconst LINK = /(<a\\b[^>]*?\\shref\\s*=\\s*)([\"'])(https?:\\/\\/[^\"']+)\\2/gi;\n\n/**\n * An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through\n * `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —\n * as the unsubscribe link when it is the one the email was rendered with, so what a click on it\n * means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the\n * end when there is none.\n */\nexport function trackEmail(\n html: string,\n tracking: MessageTracking,\n unsubscribeUrl?: string\n): string {\n const { link, pixel } = tracking;\n let out = html;\n if (link !== undefined) {\n out = out.replace(LINK, (_, start: string, quote: string, attribute: string) => {\n const url = unescapeAttribute(attribute);\n const purpose = isUnsubscribeLink(url, unsubscribeUrl) ? 'unsubscribe' : 'link';\n return `${start}${quote}${escapeAttribute(link(url, purpose))}${quote}`;\n });\n }\n if (pixel !== undefined) {\n const image = `<img src=\"${escapeAttribute(pixel)}\" width=\"1\" height=\"1\" alt=\"\" style=\"display:block;border:0;width:1px;height:1px\" />`;\n out = /<\\/body>/i.test(out) ? out.replace(/<\\/body>/i, `${image}</body>`) : `${out}${image}`;\n }\n return out;\n}\n\nfunction unescapeAttribute(value: string): string {\n return value.replaceAll('&', '&').replaceAll('"', '\"').replaceAll(''', \"'\");\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replaceAll('&', '&').replaceAll('\"', '"');\n}\n\n/** Development sender: structured logs, for eyeballing the pipeline. */\nexport class LogMessageSender implements MessageSender {\n async send(message: OutboundMessage): Promise<void> {\n const { tracking: _tracking, ...logged } = message;\n console.log(`[send] ${JSON.stringify(logged)}`);\n }\n}\n\n/**\n * Webhook sender: POST to an external delivery service (a Resend/SES gateway,\n * say). The idempotency key travels with it, so the receiver de-duplicates\n * retries and replays on idempotencyKey.\n */\nexport class WebhookMessageSender implements MessageSender {\n constructor(private readonly url: string) {}\n\n async send(message: OutboundMessage): Promise<void> {\n const response = await fetch(this.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'idempotency-key': message.idempotencyKey,\n },\n body: JSON.stringify(message),\n });\n if (!response.ok) {\n throw new Error(`message webhook failed: ${response.status}`);\n }\n }\n}\n\n/**\n * One sender per channel: the runner has a single outlet, this fans it out.\n * A channel without a sender fails the send (a misconfiguration, not a retry).\n */\nexport function routeByChannel(\n senders: Partial<Record<OutboundMessage['channel'], MessageSender>>\n): MessageSender {\n return {\n async send(message) {\n const sender = senders[message.channel];\n if (sender === undefined) {\n throw new Error(`no sender configured for channel '${message.channel}'`);\n }\n return sender.send(message);\n },\n };\n}\n\n/**\n * One push outlet per platform, chosen by the device each copy goes to (the\n * engine sends one message per device, and the host's directory says which\n * platform a token belongs to): `ios` straight to APNs, `android` through FCM,\n * say. A platform with no sender fails the step rather than guessing.\n */\nexport function routeByPlatform(senders: Record<string, MessageSender>): MessageSender {\n return {\n async send(message) {\n const platform = message.device?.platform;\n if (platform === undefined) {\n throw new Error(`push for user '${message.userId}' carries no device`);\n }\n if (!Object.hasOwn(senders, platform)) {\n throw new Error(`no push sender configured for platform '${platform}'`);\n }\n return senders[platform].send(message);\n },\n };\n}\n"],"mappings":"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"}
|