@shware/workflow 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/apns-relay.cjs +105 -0
- package/dist/apns-relay.cjs.map +1 -0
- package/dist/apns-relay.d.cts +28 -0
- package/dist/apns-relay.d.cts.map +1 -0
- package/dist/apns-relay.d.mts +28 -0
- package/dist/apns-relay.d.mts.map +1 -0
- package/dist/apns-relay.mjs +104 -0
- package/dist/apns-relay.mjs.map +1 -0
- package/dist/apns.cjs +121 -0
- package/dist/apns.cjs.map +1 -0
- package/dist/apns.d.cts +63 -0
- package/dist/apns.d.cts.map +1 -0
- package/dist/apns.d.mts +63 -0
- package/dist/apns.d.mts.map +1 -0
- package/dist/apns.mjs +116 -0
- package/dist/apns.mjs.map +1 -0
- package/dist/client.cjs +125 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +108 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +108 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +122 -0
- package/dist/client.mjs.map +1 -0
- package/dist/cloudflare/bindings.cjs +28 -0
- package/dist/cloudflare/bindings.cjs.map +1 -0
- package/dist/cloudflare/bindings.d.cts +144 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -0
- package/dist/cloudflare/bindings.d.mts +144 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -0
- package/dist/cloudflare/bindings.mjs +25 -0
- package/dist/cloudflare/bindings.mjs.map +1 -0
- package/dist/cloudflare/channels.cjs +164 -0
- package/dist/cloudflare/channels.cjs.map +1 -0
- package/dist/cloudflare/channels.d.cts +29 -0
- package/dist/cloudflare/channels.d.cts.map +1 -0
- package/dist/cloudflare/channels.d.mts +29 -0
- package/dist/cloudflare/channels.d.mts.map +1 -0
- package/dist/cloudflare/channels.mjs +161 -0
- package/dist/cloudflare/channels.mjs.map +1 -0
- package/dist/cloudflare/d1-env.cjs +17 -0
- package/dist/cloudflare/d1-env.cjs.map +1 -0
- package/dist/cloudflare/d1-env.d.cts +12 -0
- package/dist/cloudflare/d1-env.d.cts.map +1 -0
- package/dist/cloudflare/d1-env.d.mts +12 -0
- package/dist/cloudflare/d1-env.d.mts.map +1 -0
- package/dist/cloudflare/d1-env.mjs +16 -0
- package/dist/cloudflare/d1-env.mjs.map +1 -0
- package/dist/cloudflare/index.cjs +54 -0
- package/dist/cloudflare/index.d.cts +19 -0
- package/dist/cloudflare/index.d.mts +19 -0
- package/dist/cloudflare/index.mjs +16 -0
- package/dist/cloudflare/router.cjs +516 -0
- package/dist/cloudflare/router.cjs.map +1 -0
- package/dist/cloudflare/router.d.cts +157 -0
- package/dist/cloudflare/router.d.cts.map +1 -0
- package/dist/cloudflare/router.d.mts +157 -0
- package/dist/cloudflare/router.d.mts.map +1 -0
- package/dist/cloudflare/router.mjs +503 -0
- package/dist/cloudflare/router.mjs.map +1 -0
- package/dist/cloudflare/runner.cjs +225 -0
- package/dist/cloudflare/runner.cjs.map +1 -0
- package/dist/cloudflare/runner.d.cts +73 -0
- package/dist/cloudflare/runner.d.cts.map +1 -0
- package/dist/cloudflare/runner.d.mts +73 -0
- package/dist/cloudflare/runner.d.mts.map +1 -0
- package/dist/cloudflare/runner.mjs +224 -0
- package/dist/cloudflare/runner.mjs.map +1 -0
- package/dist/cloudflare/schema.sql +127 -0
- package/dist/cloudflare/senders.cjs +114 -0
- package/dist/cloudflare/senders.cjs.map +1 -0
- package/dist/cloudflare/senders.d.cts +68 -0
- package/dist/cloudflare/senders.d.cts.map +1 -0
- package/dist/cloudflare/senders.d.mts +68 -0
- package/dist/cloudflare/senders.d.mts.map +1 -0
- package/dist/cloudflare/senders.mjs +108 -0
- package/dist/cloudflare/senders.mjs.map +1 -0
- package/dist/cloudflare/worker.cjs +228 -0
- package/dist/cloudflare/worker.cjs.map +1 -0
- package/dist/cloudflare/worker.d.cts +92 -0
- package/dist/cloudflare/worker.d.cts.map +1 -0
- package/dist/cloudflare/worker.d.mts +92 -0
- package/dist/cloudflare/worker.d.mts.map +1 -0
- package/dist/cloudflare/worker.mjs +224 -0
- package/dist/cloudflare/worker.mjs.map +1 -0
- package/dist/cloudflare/workers.d.cjs +0 -0
- package/dist/cloudflare/workers.d.cts +44 -0
- package/dist/cloudflare/workers.d.cts.map +1 -0
- package/dist/cloudflare/workers.d.mts +44 -0
- package/dist/cloudflare/workers.d.mts.map +1 -0
- package/dist/config.cjs +151 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +149 -0
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +149 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +143 -0
- package/dist/config.mjs.map +1 -0
- package/dist/contract.cjs +212 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +90 -0
- package/dist/contract.d.cts.map +1 -0
- package/dist/contract.d.mts +90 -0
- package/dist/contract.d.mts.map +1 -0
- package/dist/contract.mjs +206 -0
- package/dist/contract.mjs.map +1 -0
- package/dist/dsl/action.cjs +29 -0
- package/dist/dsl/action.cjs.map +1 -0
- package/dist/dsl/action.d.cts +64 -0
- package/dist/dsl/action.d.cts.map +1 -0
- package/dist/dsl/action.d.mts +64 -0
- package/dist/dsl/action.d.mts.map +1 -0
- package/dist/dsl/action.mjs +28 -0
- package/dist/dsl/action.mjs.map +1 -0
- package/dist/dsl/base.cjs +34 -0
- package/dist/dsl/base.cjs.map +1 -0
- package/dist/dsl/base.d.cts +45 -0
- package/dist/dsl/base.d.cts.map +1 -0
- package/dist/dsl/base.d.mts +45 -0
- package/dist/dsl/base.d.mts.map +1 -0
- package/dist/dsl/base.mjs +30 -0
- package/dist/dsl/base.mjs.map +1 -0
- package/dist/dsl/bundle.cjs +207 -0
- package/dist/dsl/bundle.cjs.map +1 -0
- package/dist/dsl/bundle.d.cts +19 -0
- package/dist/dsl/bundle.d.cts.map +1 -0
- package/dist/dsl/bundle.d.mts +19 -0
- package/dist/dsl/bundle.d.mts.map +1 -0
- package/dist/dsl/bundle.mjs +206 -0
- package/dist/dsl/bundle.mjs.map +1 -0
- package/dist/dsl/condition.cjs +177 -0
- package/dist/dsl/condition.cjs.map +1 -0
- package/dist/dsl/condition.d.cts +140 -0
- package/dist/dsl/condition.d.cts.map +1 -0
- package/dist/dsl/condition.d.mts +140 -0
- package/dist/dsl/condition.d.mts.map +1 -0
- package/dist/dsl/condition.mjs +156 -0
- package/dist/dsl/condition.mjs.map +1 -0
- package/dist/dsl/flow.cjs +228 -0
- package/dist/dsl/flow.cjs.map +1 -0
- package/dist/dsl/flow.d.cts +223 -0
- package/dist/dsl/flow.d.cts.map +1 -0
- package/dist/dsl/flow.d.mts +223 -0
- package/dist/dsl/flow.d.mts.map +1 -0
- package/dist/dsl/flow.mjs +226 -0
- package/dist/dsl/flow.mjs.map +1 -0
- package/dist/dsl/index.cjs +38 -0
- package/dist/dsl/index.d.cts +10 -0
- package/dist/dsl/index.d.mts +10 -0
- package/dist/dsl/index.mjs +9 -0
- package/dist/dsl/refs.cjs +59 -0
- package/dist/dsl/refs.cjs.map +1 -0
- package/dist/dsl/refs.d.cts +88 -0
- package/dist/dsl/refs.d.cts.map +1 -0
- package/dist/dsl/refs.d.mts +88 -0
- package/dist/dsl/refs.d.mts.map +1 -0
- package/dist/dsl/refs.mjs +55 -0
- package/dist/dsl/refs.mjs.map +1 -0
- package/dist/dsl/template.cjs +65 -0
- package/dist/dsl/template.cjs.map +1 -0
- package/dist/dsl/template.d.cts +75 -0
- package/dist/dsl/template.d.cts.map +1 -0
- package/dist/dsl/template.d.mts +75 -0
- package/dist/dsl/template.d.mts.map +1 -0
- package/dist/dsl/template.mjs +63 -0
- package/dist/dsl/template.mjs.map +1 -0
- package/dist/dsl/trigger.cjs +40 -0
- package/dist/dsl/trigger.cjs.map +1 -0
- package/dist/dsl/trigger.d.cts +65 -0
- package/dist/dsl/trigger.d.cts.map +1 -0
- package/dist/dsl/trigger.d.mts +65 -0
- package/dist/dsl/trigger.d.mts.map +1 -0
- package/dist/dsl/trigger.mjs +39 -0
- package/dist/dsl/trigger.mjs.map +1 -0
- package/dist/dsl/workflow.cjs +121 -0
- package/dist/dsl/workflow.cjs.map +1 -0
- package/dist/dsl/workflow.d.cts +59 -0
- package/dist/dsl/workflow.d.cts.map +1 -0
- package/dist/dsl/workflow.d.mts +59 -0
- package/dist/dsl/workflow.d.mts.map +1 -0
- package/dist/dsl/workflow.mjs +120 -0
- package/dist/dsl/workflow.mjs.map +1 -0
- package/dist/engine/actions.cjs +27 -0
- package/dist/engine/actions.cjs.map +1 -0
- package/dist/engine/actions.d.cts +40 -0
- package/dist/engine/actions.d.cts.map +1 -0
- package/dist/engine/actions.d.mts +40 -0
- package/dist/engine/actions.d.mts.map +1 -0
- package/dist/engine/actions.mjs +26 -0
- package/dist/engine/actions.mjs.map +1 -0
- package/dist/engine/bucket.cjs +72 -0
- package/dist/engine/bucket.cjs.map +1 -0
- package/dist/engine/bucket.d.cts +32 -0
- package/dist/engine/bucket.d.cts.map +1 -0
- package/dist/engine/bucket.d.mts +32 -0
- package/dist/engine/bucket.d.mts.map +1 -0
- package/dist/engine/bucket.mjs +69 -0
- package/dist/engine/bucket.mjs.map +1 -0
- package/dist/engine/condition.cjs +166 -0
- package/dist/engine/condition.cjs.map +1 -0
- package/dist/engine/condition.d.cts +52 -0
- package/dist/engine/condition.d.cts.map +1 -0
- package/dist/engine/condition.d.mts +52 -0
- package/dist/engine/condition.d.mts.map +1 -0
- package/dist/engine/condition.mjs +161 -0
- package/dist/engine/condition.mjs.map +1 -0
- package/dist/engine/index.cjs +20 -0
- package/dist/engine/index.d.cts +8 -0
- package/dist/engine/index.d.mts +8 -0
- package/dist/engine/index.mjs +7 -0
- package/dist/engine/interpreter.cjs +428 -0
- package/dist/engine/interpreter.cjs.map +1 -0
- package/dist/engine/interpreter.d.cts +6 -0
- package/dist/engine/interpreter.d.cts.map +1 -0
- package/dist/engine/interpreter.d.mts +6 -0
- package/dist/engine/interpreter.d.mts.map +1 -0
- package/dist/engine/interpreter.mjs +427 -0
- package/dist/engine/interpreter.mjs.map +1 -0
- package/dist/engine/ports.cjs +0 -0
- package/dist/engine/ports.d.cts +252 -0
- package/dist/engine/ports.d.cts.map +1 -0
- package/dist/engine/ports.d.mts +252 -0
- package/dist/engine/ports.d.mts.map +1 -0
- package/dist/engine/ports.mjs +1 -0
- package/dist/engine/recipient.cjs +23 -0
- package/dist/engine/recipient.cjs.map +1 -0
- package/dist/engine/recipient.d.cts +13 -0
- package/dist/engine/recipient.d.cts.map +1 -0
- package/dist/engine/recipient.d.mts +13 -0
- package/dist/engine/recipient.d.mts.map +1 -0
- package/dist/engine/recipient.mjs +22 -0
- package/dist/engine/recipient.mjs.map +1 -0
- package/dist/engine/time-scale.cjs +54 -0
- package/dist/engine/time-scale.cjs.map +1 -0
- package/dist/engine/time-scale.d.cts +31 -0
- package/dist/engine/time-scale.d.cts.map +1 -0
- package/dist/engine/time-scale.d.mts +31 -0
- package/dist/engine/time-scale.d.mts.map +1 -0
- package/dist/engine/time-scale.mjs +52 -0
- package/dist/engine/time-scale.mjs.map +1 -0
- package/dist/engine/time-window.cjs +149 -0
- package/dist/engine/time-window.cjs.map +1 -0
- package/dist/engine/time-window.d.cts +22 -0
- package/dist/engine/time-window.d.cts.map +1 -0
- package/dist/engine/time-window.d.mts +22 -0
- package/dist/engine/time-window.d.mts.map +1 -0
- package/dist/engine/time-window.mjs +147 -0
- package/dist/engine/time-window.mjs.map +1 -0
- package/dist/fcm.cjs +125 -0
- package/dist/fcm.cjs.map +1 -0
- package/dist/fcm.d.cts +48 -0
- package/dist/fcm.d.cts.map +1 -0
- package/dist/fcm.d.mts +48 -0
- package/dist/fcm.d.mts.map +1 -0
- package/dist/fcm.mjs +121 -0
- package/dist/fcm.mjs.map +1 -0
- package/dist/hash.cjs +245 -0
- package/dist/hash.cjs.map +1 -0
- package/dist/hash.d.cts +30 -0
- package/dist/hash.d.cts.map +1 -0
- package/dist/hash.d.mts +30 -0
- package/dist/hash.d.mts.map +1 -0
- package/dist/hash.mjs +239 -0
- package/dist/hash.mjs.map +1 -0
- package/dist/index.cjs +100 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +17 -0
- package/dist/ir.cjs +366 -0
- package/dist/ir.cjs.map +1 -0
- package/dist/ir.d.cts +486 -0
- package/dist/ir.d.cts.map +1 -0
- package/dist/ir.d.mts +486 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +344 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/jwt.cjs +22 -0
- package/dist/jwt.cjs.map +1 -0
- package/dist/jwt.d.cts +9 -0
- package/dist/jwt.d.cts.map +1 -0
- package/dist/jwt.d.mts +9 -0
- package/dist/jwt.d.mts.map +1 -0
- package/dist/jwt.mjs +20 -0
- package/dist/jwt.mjs.map +1 -0
- package/dist/plan.cjs +223 -0
- package/dist/plan.cjs.map +1 -0
- package/dist/plan.d.cts +68 -0
- package/dist/plan.d.cts.map +1 -0
- package/dist/plan.d.mts +68 -0
- package/dist/plan.d.mts.map +1 -0
- package/dist/plan.mjs +221 -0
- package/dist/plan.mjs.map +1 -0
- package/dist/postgres-js.cjs +42 -0
- package/dist/postgres-js.cjs.map +1 -0
- package/dist/postgres-js.d.cts +28 -0
- package/dist/postgres-js.d.cts.map +1 -0
- package/dist/postgres-js.d.mts +28 -0
- package/dist/postgres-js.d.mts.map +1 -0
- package/dist/postgres-js.mjs +38 -0
- package/dist/postgres-js.mjs.map +1 -0
- package/dist/provenance.cjs +67 -0
- package/dist/provenance.cjs.map +1 -0
- package/dist/provenance.d.cts +41 -0
- package/dist/provenance.d.cts.map +1 -0
- package/dist/provenance.d.mts +41 -0
- package/dist/provenance.d.mts.map +1 -0
- package/dist/provenance.mjs +65 -0
- package/dist/provenance.mjs.map +1 -0
- package/dist/push.cjs +14 -0
- package/dist/push.cjs.map +1 -0
- package/dist/push.d.cts +16 -0
- package/dist/push.d.cts.map +1 -0
- package/dist/push.d.mts +16 -0
- package/dist/push.d.mts.map +1 -0
- package/dist/push.mjs +13 -0
- package/dist/push.mjs.map +1 -0
- package/dist/react-email.cjs +48 -0
- package/dist/react-email.cjs.map +1 -0
- package/dist/react-email.d.cts +7 -0
- package/dist/react-email.d.cts.map +1 -0
- package/dist/react-email.d.mts +7 -0
- package/dist/react-email.d.mts.map +1 -0
- package/dist/react-email.mjs +47 -0
- package/dist/react-email.mjs.map +1 -0
- package/dist/reporting/classify.cjs +42 -0
- package/dist/reporting/classify.cjs.map +1 -0
- package/dist/reporting/classify.d.cts +60 -0
- package/dist/reporting/classify.d.cts.map +1 -0
- package/dist/reporting/classify.d.mts +60 -0
- package/dist/reporting/classify.d.mts.map +1 -0
- package/dist/reporting/classify.mjs +38 -0
- package/dist/reporting/classify.mjs.map +1 -0
- package/dist/reporting/observer.cjs +63 -0
- package/dist/reporting/observer.cjs.map +1 -0
- package/dist/reporting/observer.d.cts +18 -0
- package/dist/reporting/observer.d.cts.map +1 -0
- package/dist/reporting/observer.d.mts +18 -0
- package/dist/reporting/observer.d.mts.map +1 -0
- package/dist/reporting/observer.mjs +62 -0
- package/dist/reporting/observer.mjs.map +1 -0
- package/dist/reporting/tracking.cjs +153 -0
- package/dist/reporting/tracking.cjs.map +1 -0
- package/dist/reporting/tracking.d.cts +43 -0
- package/dist/reporting/tracking.d.cts.map +1 -0
- package/dist/reporting/tracking.d.mts +43 -0
- package/dist/reporting/tracking.d.mts.map +1 -0
- package/dist/reporting/tracking.mjs +151 -0
- package/dist/reporting/tracking.mjs.map +1 -0
- package/dist/runtime.cjs +19 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +30 -0
- package/dist/runtime.d.cts.map +1 -0
- package/dist/runtime.d.mts +30 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +18 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store/d1.cjs +185 -0
- package/dist/store/d1.cjs.map +1 -0
- package/dist/store/d1.d.cts +55 -0
- package/dist/store/d1.d.cts.map +1 -0
- package/dist/store/d1.d.mts +55 -0
- package/dist/store/d1.d.mts.map +1 -0
- package/dist/store/d1.mjs +184 -0
- package/dist/store/d1.mjs.map +1 -0
- package/dist/store/facts.cjs +60 -0
- package/dist/store/facts.cjs.map +1 -0
- package/dist/store/facts.d.cts +38 -0
- package/dist/store/facts.d.cts.map +1 -0
- package/dist/store/facts.d.mts +38 -0
- package/dist/store/facts.d.mts.map +1 -0
- package/dist/store/facts.mjs +58 -0
- package/dist/store/facts.mjs.map +1 -0
- package/dist/store/index.cjs +31 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +291 -0
- package/dist/store/index.d.cts.map +1 -0
- package/dist/store/index.d.mts +291 -0
- package/dist/store/index.d.mts.map +1 -0
- package/dist/store/index.mjs +22 -0
- package/dist/store/index.mjs.map +1 -0
- package/dist/store/postgres-reporting.cjs +213 -0
- package/dist/store/postgres-reporting.cjs.map +1 -0
- package/dist/store/postgres-reporting.d.cts +63 -0
- package/dist/store/postgres-reporting.d.cts.map +1 -0
- package/dist/store/postgres-reporting.d.mts +63 -0
- package/dist/store/postgres-reporting.d.mts.map +1 -0
- package/dist/store/postgres-reporting.mjs +212 -0
- package/dist/store/postgres-reporting.mjs.map +1 -0
- package/dist/store/postgres.cjs +309 -0
- package/dist/store/postgres.cjs.map +1 -0
- package/dist/store/postgres.d.cts +77 -0
- package/dist/store/postgres.d.cts.map +1 -0
- package/dist/store/postgres.d.mts +77 -0
- package/dist/store/postgres.d.mts.map +1 -0
- package/dist/store/postgres.mjs +306 -0
- package/dist/store/postgres.mjs.map +1 -0
- package/dist/store/sql.cjs +24 -0
- package/dist/store/sql.cjs.map +1 -0
- package/dist/store/sql.d.cts +25 -0
- package/dist/store/sql.d.cts.map +1 -0
- package/dist/store/sql.d.mts +25 -0
- package/dist/store/sql.d.mts.map +1 -0
- package/dist/store/sql.mjs +23 -0
- package/dist/store/sql.mjs.map +1 -0
- package/dist/store/tables.cjs +404 -0
- package/dist/store/tables.cjs.map +1 -0
- package/dist/store/tables.d.cts +81 -0
- package/dist/store/tables.d.cts.map +1 -0
- package/dist/store/tables.d.mts +81 -0
- package/dist/store/tables.d.mts.map +1 -0
- package/dist/store/tables.mjs +399 -0
- package/dist/store/tables.mjs.map +1 -0
- package/dist/templates.cjs +247 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +187 -0
- package/dist/templates.d.cts.map +1 -0
- package/dist/templates.d.mts +187 -0
- package/dist/templates.d.mts.map +1 -0
- package/dist/templates.mjs +230 -0
- package/dist/templates.mjs.map +1 -0
- package/dist/webhooks.cjs +96 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +25 -0
- package/dist/webhooks.d.cts.map +1 -0
- package/dist/webhooks.d.mts +25 -0
- package/dist/webhooks.d.mts.map +1 -0
- package/dist/webhooks.mjs +94 -0
- package/dist/webhooks.mjs.map +1 -0
- package/dist/webpush.cjs +180 -0
- package/dist/webpush.cjs.map +1 -0
- package/dist/webpush.d.cts +53 -0
- package/dist/webpush.d.cts.map +1 -0
- package/dist/webpush.d.mts +53 -0
- package/dist/webpush.d.mts.map +1 -0
- package/dist/webpush.mjs +175 -0
- package/dist/webpush.mjs.map +1 -0
- package/package.json +189 -3
- package/src/cloudflare/schema.sql +127 -0
- package/src/cloudflare/workers.d.ts +44 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.mjs","names":[],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\nimport { ingestEvent } from './router';\nimport { LogMessageSender, WebhookMessageSender } from './senders';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock can tell them apart (see wakeExpired).\n */\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A template refusing its props, or the contract refusing an emitted event,\n * is deterministic — a retry hits the same refusal — so it ends the step at\n * once instead of burning the retry budget.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && NON_RETRYABLE.has(error.name)) {\n throw new NonRetryableError(error.message, error.name);\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 + KV from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * CfEmailSender with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n return await this.execute(event, step, store);\n } finally {\n await this.closeStore(store);\n }\n }\n\n private async execute(\n event: WorkflowEvent<JourneyParams>,\n step: WorkflowStep,\n store: JourneyStore\n ): Promise<JourneyOutcome> {\n const { workflowId, contentHash, userId } = event.payload;\n const env = this.env;\n\n const stored = await store.getWorkflowIR(contentHash);\n if (stored === undefined) {\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n }\n const runtime = resolveRuntime({ current: this.runtime(event.payload) }, 'current');\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n\n const observer = this.createObserver(store);\n const finished = (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId: event.instanceId,\n userId,\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages\n ? logging(this.createMessageSender())\n : this.createMessageSender();\n\n // send_event feeds straight back into the router logic — an in-Worker call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const contract = this.contract();\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId, event: name, payload: accepted }, store);\n },\n };\n\n let outcome: JourneyOutcome;\n try {\n outcome = await runJourney(ir, {\n userId,\n instanceId: event.instanceId,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n facts: new JourneyFactSource(store, userId),\n messages,\n events,\n actions: this.createActionInvoker(),\n validator: this.createMessageValidator(),\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions are deliberately left in place. They are cleaned up on\n * every ordinary ending, but here the wake rows are the only evidence of\n * what the instance was waiting for, and dropping them would also break\n * the instance if the platform does re-run it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(event.instanceId, 'failed');\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(event.instanceId, outcome.status);\n await store.unsubscribe(event.instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
-- Journey engine data plane (D1). Conditions are evaluated by querying at
|
|
2
|
+
-- decision time: lazy, with no real-time segment materialisation.
|
|
3
|
+
|
|
4
|
+
-- Raw event log. Every count- and window-based condition reads this table, so a
|
|
5
|
+
-- duplicated row silently changes a journey's behaviour, and `id` is what stops
|
|
6
|
+
-- that: it is the occurrence's identity, chosen by whoever sent it, and the key
|
|
7
|
+
-- constraint makes a second delivery of the same occurrence a no-op. Ingest is
|
|
8
|
+
-- reachable from two retrying callers (a client retrying a 500, and the
|
|
9
|
+
-- interpreter's send_event step being retried after it already committed its
|
|
10
|
+
-- row), so both pass an id they can reproduce.
|
|
11
|
+
--
|
|
12
|
+
-- Only the sender knows whether two deliveries describe one occurrence or two,
|
|
13
|
+
-- so the engine never invents that answer: an ingest that arrives without an id
|
|
14
|
+
-- is given a fresh one, which is a truthful "this is its own occurrence" rather
|
|
15
|
+
-- than a guess.
|
|
16
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
17
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
18
|
+
user_id TEXT NOT NULL,
|
|
19
|
+
name TEXT NOT NULL,
|
|
20
|
+
ts INTEGER NOT NULL,
|
|
21
|
+
payload TEXT NOT NULL DEFAULT '{}'
|
|
22
|
+
);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_events_user_name_ts ON events (user_id, name, ts);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
26
|
+
user_id TEXT PRIMARY KEY,
|
|
27
|
+
props TEXT NOT NULL DEFAULT '{}'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- Deploy artefacts: segment definitions (resolved by id during evaluation)
|
|
31
|
+
-- and the trigger routing table.
|
|
32
|
+
-- Push devices: one row per provider token, owned by the user who registered it last.
|
|
33
|
+
-- user_id is null for a device whose user signed out: it stays registered until the next
|
|
34
|
+
-- sign-in binds it again. A database created while it was NOT NULL needs the table rebuilt.
|
|
35
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
36
|
+
token TEXT PRIMARY KEY,
|
|
37
|
+
user_id TEXT,
|
|
38
|
+
platform TEXT NOT NULL,
|
|
39
|
+
ts INTEGER NOT NULL
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_devices_user_ts ON devices (user_id, ts);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS segments (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
condition TEXT NOT NULL,
|
|
46
|
+
hash TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
-- Event trigger routes. `where` gates on the triggering event's payload
|
|
50
|
+
-- (a payload-only condition tree); `filter` gates on the profile.
|
|
51
|
+
CREATE TABLE IF NOT EXISTS triggers (
|
|
52
|
+
workflow TEXT PRIMARY KEY,
|
|
53
|
+
hash TEXT NOT NULL,
|
|
54
|
+
event TEXT NOT NULL,
|
|
55
|
+
"where" TEXT,
|
|
56
|
+
filter TEXT
|
|
57
|
+
);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_triggers_event ON triggers (event);
|
|
59
|
+
|
|
60
|
+
-- Segment-entry trigger routes.
|
|
61
|
+
CREATE TABLE IF NOT EXISTS segment_triggers (
|
|
62
|
+
workflow TEXT PRIMARY KEY,
|
|
63
|
+
hash TEXT NOT NULL,
|
|
64
|
+
segment TEXT NOT NULL
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_segment_triggers_segment ON segment_triggers (segment);
|
|
67
|
+
|
|
68
|
+
-- Lazily maintained segment membership, covering only segments that something
|
|
69
|
+
-- triggers on. Transitions are re-evaluated when the user is ingested or
|
|
70
|
+
-- identified, and by the scheduled sweep (journeyWorker's `scheduled`), which
|
|
71
|
+
-- also catches drift driven purely by the clock.
|
|
72
|
+
CREATE TABLE IF NOT EXISTS segment_members (
|
|
73
|
+
segment TEXT NOT NULL,
|
|
74
|
+
user_id TEXT NOT NULL,
|
|
75
|
+
ts INTEGER NOT NULL,
|
|
76
|
+
PRIMARY KEY (segment, user_id)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- Workflows a segment entry still has to start for a member: written with the
|
|
80
|
+
-- membership, removed as each starts, so a failed start is retried by the next
|
|
81
|
+
-- evaluation instead of being hidden behind the membership.
|
|
82
|
+
CREATE TABLE IF NOT EXISTS segment_starts (
|
|
83
|
+
segment TEXT NOT NULL,
|
|
84
|
+
workflow TEXT NOT NULL,
|
|
85
|
+
user_id TEXT NOT NULL,
|
|
86
|
+
ts INTEGER NOT NULL,
|
|
87
|
+
PRIMARY KEY (segment, workflow, user_id)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- Erasures in progress (forgetUser): the user's data stays until every running
|
|
91
|
+
-- instance is stopped, and the scheduled sweep retries what is left.
|
|
92
|
+
CREATE TABLE IF NOT EXISTS erasures (
|
|
93
|
+
user_id TEXT PRIMARY KEY,
|
|
94
|
+
ts INTEGER NOT NULL
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
-- Entry ledger: the once-per-user policy, instance addressing, and the audit
|
|
98
|
+
-- trail of which IR version each user was pinned to.
|
|
99
|
+
--
|
|
100
|
+
-- The primary key is what makes the policy atomic, and it is also the whole
|
|
101
|
+
-- policy: a user who has entered cannot enter again. The one exception is a
|
|
102
|
+
-- row left at 'failed', which records an instance that died rather than a
|
|
103
|
+
-- journey the user received; the router takes such a row over so the entry can
|
|
104
|
+
-- be retried (see startJourney). A general re-entry policy is a separate
|
|
105
|
+
-- design question and deliberately not encoded here.
|
|
106
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
107
|
+
workflow TEXT NOT NULL,
|
|
108
|
+
user_id TEXT NOT NULL,
|
|
109
|
+
instance_id TEXT NOT NULL,
|
|
110
|
+
hash TEXT NOT NULL,
|
|
111
|
+
status TEXT NOT NULL,
|
|
112
|
+
ts INTEGER NOT NULL,
|
|
113
|
+
PRIMARY KEY (workflow, user_id)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- wait_until wake routing. `wake_handle` is an opaque handle (the instance id
|
|
117
|
+
-- on Cloudflare, a callback id on AWS). The primary key makes the interpreter's
|
|
118
|
+
-- re-subscription on every wait attempt idempotent, which its one-shot-callback
|
|
119
|
+
-- contract requires.
|
|
120
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
121
|
+
user_id TEXT NOT NULL,
|
|
122
|
+
event TEXT NOT NULL,
|
|
123
|
+
wake_handle TEXT NOT NULL,
|
|
124
|
+
ts INTEGER NOT NULL,
|
|
125
|
+
PRIMARY KEY (wake_handle, event)
|
|
126
|
+
);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_subs_user_event ON subscriptions (user_id, event);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
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 } = await this.options.render(message.template, message.props);
|
|
18
|
+
const tracked = message.tracking === void 0 ? html : trackEmail(html, message.tracking);
|
|
19
|
+
await message.tracking?.commit?.();
|
|
20
|
+
await this.options.binding.send({
|
|
21
|
+
from: this.options.from,
|
|
22
|
+
to: message.recipient,
|
|
23
|
+
...this.options.replyTo === void 0 ? {} : { replyTo: this.options.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
|
+
* and the open pixel appended before `</body>`, or at the end when there is none.
|
|
35
|
+
*/
|
|
36
|
+
function trackEmail(html, tracking) {
|
|
37
|
+
const { link, pixel } = tracking;
|
|
38
|
+
let out = html;
|
|
39
|
+
if (link !== void 0) out = out.replace(LINK, (_, start, quote, url) => `${start}${quote}${escapeAttribute(link(unescapeAttribute(url)))}${quote}`);
|
|
40
|
+
if (pixel !== void 0) {
|
|
41
|
+
const image = `<img src="${escapeAttribute(pixel)}" width="1" height="1" alt="" style="display:block;border:0;width:1px;height:1px" />`;
|
|
42
|
+
out = /<\/body>/i.test(out) ? out.replace(/<\/body>/i, `${image}</body>`) : `${out}${image}`;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function unescapeAttribute(value) {
|
|
47
|
+
return value.replaceAll("&", "&").replaceAll(""", "\"").replaceAll("'", "'");
|
|
48
|
+
}
|
|
49
|
+
function escapeAttribute(value) {
|
|
50
|
+
return value.replaceAll("&", "&").replaceAll("\"", """);
|
|
51
|
+
}
|
|
52
|
+
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
53
|
+
var LogMessageSender = class {
|
|
54
|
+
async send(message) {
|
|
55
|
+
const { tracking: _tracking, ...logged } = message;
|
|
56
|
+
console.log(`[send] ${JSON.stringify(logged)}`);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
61
|
+
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
62
|
+
* retries and replays on idempotencyKey.
|
|
63
|
+
*/
|
|
64
|
+
var WebhookMessageSender = class {
|
|
65
|
+
url;
|
|
66
|
+
constructor(url) {
|
|
67
|
+
this.url = url;
|
|
68
|
+
}
|
|
69
|
+
async send(message) {
|
|
70
|
+
const response = await fetch(this.url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"content-type": "application/json",
|
|
74
|
+
"idempotency-key": message.idempotencyKey
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(message)
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) throw new Error(`message webhook failed: ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
83
|
+
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
84
|
+
*/
|
|
85
|
+
function routeByChannel(senders) {
|
|
86
|
+
return { async send(message) {
|
|
87
|
+
const sender = senders[message.channel];
|
|
88
|
+
if (sender === void 0) throw new Error(`no sender configured for channel '${message.channel}'`);
|
|
89
|
+
return sender.send(message);
|
|
90
|
+
} };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
94
|
+
* engine sends one message per device, and the host's directory says which
|
|
95
|
+
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
96
|
+
* say. A platform with no sender fails the step rather than guessing.
|
|
97
|
+
*/
|
|
98
|
+
function routeByPlatform(senders) {
|
|
99
|
+
return { async send(message) {
|
|
100
|
+
const platform = message.device?.platform;
|
|
101
|
+
if (platform === void 0) throw new Error(`push for user '${message.userId}' carries no device`);
|
|
102
|
+
if (!Object.hasOwn(senders, platform)) throw new Error(`no push sender configured for platform '${platform}'`);
|
|
103
|
+
return senders[platform].send(message);
|
|
104
|
+
} };
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
exports.CfEmailSender = CfEmailSender;
|
|
108
|
+
exports.LogMessageSender = LogMessageSender;
|
|
109
|
+
exports.WebhookMessageSender = WebhookMessageSender;
|
|
110
|
+
exports.routeByChannel = routeByChannel;
|
|
111
|
+
exports.routeByPlatform = routeByPlatform;
|
|
112
|
+
exports.trackEmail = trackEmail;
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=senders.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senders.cjs","names":[],"sources":["../../src/cloudflare/senders.ts"],"sourcesContent":["import type { EmailAddress } from '../config';\nimport type { MessageSender, MessageTracking, OutboundMessage } from '../engine';\nimport type { ScalarIR } from '../ir';\nimport type { EmailBindingLike } from './bindings';\n\nexport type { EmailAddress } from '../config';\nexport type { EmailBindingLike } from './bindings';\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.\n */\nexport type EmailRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<{ subject: string; html: string }>;\n\nexport interface CfEmailOptions {\n /** The Worker's `send_email` binding. */\n binding: EmailBindingLike;\n from: EmailAddress;\n replyTo?: EmailAddress;\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 } = await this.options.render(message.template, message.props);\n const tracked = message.tracking === undefined ? html : trackEmail(html, message.tracking);\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: this.options.from,\n to: message.recipient,\n ...(this.options.replyTo === undefined ? {} : { replyTo: this.options.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 * and the open pixel appended before `</body>`, or at the end when there is none.\n */\nexport function trackEmail(html: string, tracking: MessageTracking): string {\n const { link, pixel } = tracking;\n let out = html;\n if (link !== undefined) {\n out = out.replace(\n LINK,\n (_, start: string, quote: string, url: string) =>\n `${start}${quote}${escapeAttribute(link(unescapeAttribute(url)))}${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":";;;;;;;;AAgCA,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,SAAS,MAAM,KAAK,QAAQ,OAAO,QAAQ,UAAU,QAAQ,KAAK;EACnF,MAAM,UAAU,QAAQ,aAAa,KAAA,IAAY,OAAO,WAAW,MAAM,QAAQ,QAAQ;EAEzF,MAAM,QAAQ,UAAU,SAAS;EACjC,MAAM,KAAK,QAAQ,QAAQ,KAAK;GAC9B,MAAM,KAAK,QAAQ;GACnB,IAAI,QAAQ;GACZ,GAAI,KAAK,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ,QAAQ;GAC9E;GACA,MAAM;GACN,gBAAgB,QAAQ;EAC1B,CAAC;CACH;AACF;AAEA,MAAM,OAAO;;;;;;AAOb,SAAgB,WAAW,MAAc,UAAmC;CAC1E,MAAM,EAAE,MAAM,UAAU;CACxB,IAAI,MAAM;CACV,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,QACR,OACC,GAAG,OAAe,OAAe,QAChC,GAAG,QAAQ,QAAQ,gBAAgB,KAAK,kBAAkB,GAAG,CAAC,CAAC,IAAI,OACvE;CAEF,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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ScalarIR } from "../ir.cjs";
|
|
2
|
+
import { MessageSender, MessageTracking, OutboundMessage } from "../engine/ports.cjs";
|
|
3
|
+
import "../engine/index.cjs";
|
|
4
|
+
import { EmailAddress } from "../config.cjs";
|
|
5
|
+
import { EmailBindingLike } from "./bindings.cjs";
|
|
6
|
+
//#region src/cloudflare/senders.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Template renderer, injected by the app — this package's core stays free of
|
|
9
|
+
* any react-email dependency (`@shware/workflow/react-email` ships one over a
|
|
10
|
+
* registry). The subject comes back final: it is sent exactly as written.
|
|
11
|
+
*/
|
|
12
|
+
export type EmailRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<{
|
|
13
|
+
subject: string;
|
|
14
|
+
html: string;
|
|
15
|
+
}>;
|
|
16
|
+
export interface CfEmailOptions {
|
|
17
|
+
/** The Worker's `send_email` binding. */
|
|
18
|
+
binding: EmailBindingLike;
|
|
19
|
+
from: EmailAddress;
|
|
20
|
+
replyTo?: EmailAddress;
|
|
21
|
+
render: EmailRenderer;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Send straight through Cloudflare Email Service — a binding call, no outbound
|
|
25
|
+
* HTTP. Throwing hands retries to the CF step, and the message's
|
|
26
|
+
* idempotencyKey travels with the call so the binding can drop the duplicate a
|
|
27
|
+
* retry or replay produces.
|
|
28
|
+
*/
|
|
29
|
+
export declare class CfEmailSender implements MessageSender {
|
|
30
|
+
private readonly options;
|
|
31
|
+
constructor(options: CfEmailOptions);
|
|
32
|
+
send(message: OutboundMessage): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
36
|
+
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back),
|
|
37
|
+
* and the open pixel appended before `</body>`, or at the end when there is none.
|
|
38
|
+
*/
|
|
39
|
+
export declare function trackEmail(html: string, tracking: MessageTracking): string;
|
|
40
|
+
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
41
|
+
export declare class LogMessageSender implements MessageSender {
|
|
42
|
+
send(message: OutboundMessage): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
46
|
+
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
47
|
+
* retries and replays on idempotencyKey.
|
|
48
|
+
*/
|
|
49
|
+
export declare class WebhookMessageSender implements MessageSender {
|
|
50
|
+
private readonly url;
|
|
51
|
+
constructor(url: string);
|
|
52
|
+
send(message: OutboundMessage): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
56
|
+
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
57
|
+
*/
|
|
58
|
+
export declare function routeByChannel(senders: Partial<Record<OutboundMessage['channel'], MessageSender>>): MessageSender;
|
|
59
|
+
/**
|
|
60
|
+
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
61
|
+
* engine sends one message per device, and the host's directory says which
|
|
62
|
+
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
63
|
+
* say. A platform with no sender fails the step rather than guessing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function routeByPlatform(senders: Record<string, MessageSender>): MessageSender;
|
|
66
|
+
//#endregion
|
|
67
|
+
export type { EmailAddress, EmailBindingLike };
|
|
68
|
+
//# sourceMappingURL=senders.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senders.d.cts","names":[],"sources":["../../src/cloudflare/senders.ts"],"mappings":";;;;;;;;;;;YAaY,iBACV,kBACA,OAAO,eAAe,0BACnB;EAAU;EAAiB;;iBAEf;;EAEf,SAAS;EACT,MAAM;EACN,UAAU;EACV,QAAQ;;;;;;;;qBASG,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB;;;;;;;wBA6BxB,WAAW,cAAc,UAAU;;qBA0BtC,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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ScalarIR } from "../ir.mjs";
|
|
2
|
+
import { MessageSender, MessageTracking, OutboundMessage } from "../engine/ports.mjs";
|
|
3
|
+
import "../engine/index.mjs";
|
|
4
|
+
import { EmailAddress } from "../config.mjs";
|
|
5
|
+
import { EmailBindingLike } from "./bindings.mjs";
|
|
6
|
+
//#region src/cloudflare/senders.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Template renderer, injected by the app — this package's core stays free of
|
|
9
|
+
* any react-email dependency (`@shware/workflow/react-email` ships one over a
|
|
10
|
+
* registry). The subject comes back final: it is sent exactly as written.
|
|
11
|
+
*/
|
|
12
|
+
export type EmailRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<{
|
|
13
|
+
subject: string;
|
|
14
|
+
html: string;
|
|
15
|
+
}>;
|
|
16
|
+
export interface CfEmailOptions {
|
|
17
|
+
/** The Worker's `send_email` binding. */
|
|
18
|
+
binding: EmailBindingLike;
|
|
19
|
+
from: EmailAddress;
|
|
20
|
+
replyTo?: EmailAddress;
|
|
21
|
+
render: EmailRenderer;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Send straight through Cloudflare Email Service — a binding call, no outbound
|
|
25
|
+
* HTTP. Throwing hands retries to the CF step, and the message's
|
|
26
|
+
* idempotencyKey travels with the call so the binding can drop the duplicate a
|
|
27
|
+
* retry or replay produces.
|
|
28
|
+
*/
|
|
29
|
+
export declare class CfEmailSender implements MessageSender {
|
|
30
|
+
private readonly options;
|
|
31
|
+
constructor(options: CfEmailOptions);
|
|
32
|
+
send(message: OutboundMessage): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
36
|
+
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back),
|
|
37
|
+
* and the open pixel appended before `</body>`, or at the end when there is none.
|
|
38
|
+
*/
|
|
39
|
+
export declare function trackEmail(html: string, tracking: MessageTracking): string;
|
|
40
|
+
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
41
|
+
export declare class LogMessageSender implements MessageSender {
|
|
42
|
+
send(message: OutboundMessage): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
46
|
+
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
47
|
+
* retries and replays on idempotencyKey.
|
|
48
|
+
*/
|
|
49
|
+
export declare class WebhookMessageSender implements MessageSender {
|
|
50
|
+
private readonly url;
|
|
51
|
+
constructor(url: string);
|
|
52
|
+
send(message: OutboundMessage): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
56
|
+
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
57
|
+
*/
|
|
58
|
+
export declare function routeByChannel(senders: Partial<Record<OutboundMessage['channel'], MessageSender>>): MessageSender;
|
|
59
|
+
/**
|
|
60
|
+
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
61
|
+
* engine sends one message per device, and the host's directory says which
|
|
62
|
+
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
63
|
+
* say. A platform with no sender fails the step rather than guessing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function routeByPlatform(senders: Record<string, MessageSender>): MessageSender;
|
|
66
|
+
//#endregion
|
|
67
|
+
export type { EmailAddress, EmailBindingLike };
|
|
68
|
+
//# sourceMappingURL=senders.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senders.d.mts","names":[],"sources":["../../src/cloudflare/senders.ts"],"mappings":";;;;;;;;;;;YAaY,iBACV,kBACA,OAAO,eAAe,0BACnB;EAAU;EAAiB;;iBAEf;;EAEf,SAAS;EACT,MAAM;EACN,UAAU;EACV,QAAQ;;;;;;;;qBASG,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB;;;;;;;wBA6BxB,WAAW,cAAc,UAAU;;qBA0BtC,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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//#region src/cloudflare/senders.ts
|
|
2
|
+
/**
|
|
3
|
+
* Send straight through Cloudflare Email Service — a binding call, no outbound
|
|
4
|
+
* HTTP. Throwing hands retries to the CF step, and the message's
|
|
5
|
+
* idempotencyKey travels with the call so the binding can drop the duplicate a
|
|
6
|
+
* retry or replay produces.
|
|
7
|
+
*/
|
|
8
|
+
var CfEmailSender = class {
|
|
9
|
+
options;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
async send(message) {
|
|
14
|
+
if (message.channel !== "email") throw new Error(`CfEmailSender: unsupported channel '${message.channel}'`);
|
|
15
|
+
if (message.recipient === void 0) throw new Error(`CfEmailSender: no recipient for user '${message.userId}'`);
|
|
16
|
+
const { subject, html } = await this.options.render(message.template, message.props);
|
|
17
|
+
const tracked = message.tracking === void 0 ? html : trackEmail(html, message.tracking);
|
|
18
|
+
await message.tracking?.commit?.();
|
|
19
|
+
await this.options.binding.send({
|
|
20
|
+
from: this.options.from,
|
|
21
|
+
to: message.recipient,
|
|
22
|
+
...this.options.replyTo === void 0 ? {} : { replyTo: this.options.replyTo },
|
|
23
|
+
subject,
|
|
24
|
+
html: tracked,
|
|
25
|
+
idempotencyKey: message.idempotencyKey
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const LINK = /(<a\b[^>]*?\shref\s*=\s*)(["'])(https?:\/\/[^"']+)\2/gi;
|
|
30
|
+
/**
|
|
31
|
+
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
32
|
+
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back),
|
|
33
|
+
* and the open pixel appended before `</body>`, or at the end when there is none.
|
|
34
|
+
*/
|
|
35
|
+
function trackEmail(html, tracking) {
|
|
36
|
+
const { link, pixel } = tracking;
|
|
37
|
+
let out = html;
|
|
38
|
+
if (link !== void 0) out = out.replace(LINK, (_, start, quote, url) => `${start}${quote}${escapeAttribute(link(unescapeAttribute(url)))}${quote}`);
|
|
39
|
+
if (pixel !== void 0) {
|
|
40
|
+
const image = `<img src="${escapeAttribute(pixel)}" width="1" height="1" alt="" style="display:block;border:0;width:1px;height:1px" />`;
|
|
41
|
+
out = /<\/body>/i.test(out) ? out.replace(/<\/body>/i, `${image}</body>`) : `${out}${image}`;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function unescapeAttribute(value) {
|
|
46
|
+
return value.replaceAll("&", "&").replaceAll(""", "\"").replaceAll("'", "'");
|
|
47
|
+
}
|
|
48
|
+
function escapeAttribute(value) {
|
|
49
|
+
return value.replaceAll("&", "&").replaceAll("\"", """);
|
|
50
|
+
}
|
|
51
|
+
/** Development sender: structured logs, for eyeballing the pipeline. */
|
|
52
|
+
var LogMessageSender = class {
|
|
53
|
+
async send(message) {
|
|
54
|
+
const { tracking: _tracking, ...logged } = message;
|
|
55
|
+
console.log(`[send] ${JSON.stringify(logged)}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Webhook sender: POST to an external delivery service (a Resend/SES gateway,
|
|
60
|
+
* say). The idempotency key travels with it, so the receiver de-duplicates
|
|
61
|
+
* retries and replays on idempotencyKey.
|
|
62
|
+
*/
|
|
63
|
+
var WebhookMessageSender = class {
|
|
64
|
+
url;
|
|
65
|
+
constructor(url) {
|
|
66
|
+
this.url = url;
|
|
67
|
+
}
|
|
68
|
+
async send(message) {
|
|
69
|
+
const response = await fetch(this.url, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"content-type": "application/json",
|
|
73
|
+
"idempotency-key": message.idempotencyKey
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(message)
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) throw new Error(`message webhook failed: ${response.status}`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* One sender per channel: the runner has a single outlet, this fans it out.
|
|
82
|
+
* A channel without a sender fails the send (a misconfiguration, not a retry).
|
|
83
|
+
*/
|
|
84
|
+
function routeByChannel(senders) {
|
|
85
|
+
return { async send(message) {
|
|
86
|
+
const sender = senders[message.channel];
|
|
87
|
+
if (sender === void 0) throw new Error(`no sender configured for channel '${message.channel}'`);
|
|
88
|
+
return sender.send(message);
|
|
89
|
+
} };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One push outlet per platform, chosen by the device each copy goes to (the
|
|
93
|
+
* engine sends one message per device, and the host's directory says which
|
|
94
|
+
* platform a token belongs to): `ios` straight to APNs, `android` through FCM,
|
|
95
|
+
* say. A platform with no sender fails the step rather than guessing.
|
|
96
|
+
*/
|
|
97
|
+
function routeByPlatform(senders) {
|
|
98
|
+
return { async send(message) {
|
|
99
|
+
const platform = message.device?.platform;
|
|
100
|
+
if (platform === void 0) throw new Error(`push for user '${message.userId}' carries no device`);
|
|
101
|
+
if (!Object.hasOwn(senders, platform)) throw new Error(`no push sender configured for platform '${platform}'`);
|
|
102
|
+
return senders[platform].send(message);
|
|
103
|
+
} };
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { CfEmailSender, LogMessageSender, WebhookMessageSender, routeByChannel, routeByPlatform, trackEmail };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=senders.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"senders.mjs","names":[],"sources":["../../src/cloudflare/senders.ts"],"sourcesContent":["import type { EmailAddress } from '../config';\nimport type { MessageSender, MessageTracking, OutboundMessage } from '../engine';\nimport type { ScalarIR } from '../ir';\nimport type { EmailBindingLike } from './bindings';\n\nexport type { EmailAddress } from '../config';\nexport type { EmailBindingLike } from './bindings';\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.\n */\nexport type EmailRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<{ subject: string; html: string }>;\n\nexport interface CfEmailOptions {\n /** The Worker's `send_email` binding. */\n binding: EmailBindingLike;\n from: EmailAddress;\n replyTo?: EmailAddress;\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 } = await this.options.render(message.template, message.props);\n const tracked = message.tracking === undefined ? html : trackEmail(html, message.tracking);\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: this.options.from,\n to: message.recipient,\n ...(this.options.replyTo === undefined ? {} : { replyTo: this.options.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 * and the open pixel appended before `</body>`, or at the end when there is none.\n */\nexport function trackEmail(html: string, tracking: MessageTracking): string {\n const { link, pixel } = tracking;\n let out = html;\n if (link !== undefined) {\n out = out.replace(\n LINK,\n (_, start: string, quote: string, url: string) =>\n `${start}${quote}${escapeAttribute(link(unescapeAttribute(url)))}${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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"}
|