@shware/workflow 0.0.0-stage → 0.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/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,47 @@
|
|
|
1
|
+
import { parseTemplateProps } from "./templates.mjs";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
4
|
+
//#region src/react-email.ts
|
|
5
|
+
/**
|
|
6
|
+
* `@shware/workflow/react-email` — an EmailRenderer over the project's email
|
|
7
|
+
* registry (`react-dom` is an optional peer dependency; import this subpath
|
|
8
|
+
* only when you use it).
|
|
9
|
+
*
|
|
10
|
+
* The registry maps keys to `emailTemplate({...})` definitions. Props are
|
|
11
|
+
* parsed against the template's schema before the component renders — a
|
|
12
|
+
* failure throws TemplateValidationError and nothing renders — and the
|
|
13
|
+
* component renders as an element, so hooks and context work. Rendering
|
|
14
|
+
* streams and waits for every Suspense boundary (`allReady`) — react-email's
|
|
15
|
+
* `<Tailwind>` suspends while it compiles the classes, which synchronous
|
|
16
|
+
* static markup cannot wait for — then prepends the XHTML doctype react-email
|
|
17
|
+
* uses. @react-email/render itself is avoided because its Workers build
|
|
18
|
+
* statically imports prettier and html-to-text for options never used here.
|
|
19
|
+
*/
|
|
20
|
+
const DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
21
|
+
function registryRenderer(emails) {
|
|
22
|
+
return async (key, props) => {
|
|
23
|
+
if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);
|
|
24
|
+
const template = emails[key];
|
|
25
|
+
const parsed = parseTemplateProps(template, key, props);
|
|
26
|
+
const element = createElement(template.component, parsed);
|
|
27
|
+
return {
|
|
28
|
+
subject: template.subject,
|
|
29
|
+
html: DOCTYPE + await renderToHtml(element)
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */
|
|
34
|
+
async function renderToHtml(element) {
|
|
35
|
+
let failure;
|
|
36
|
+
const stream = await renderToReadableStream(element, { onError: (error) => {
|
|
37
|
+
failure = error;
|
|
38
|
+
} });
|
|
39
|
+
await stream.allReady;
|
|
40
|
+
const html = await new Response(stream).text();
|
|
41
|
+
if (failure !== void 0) throw failure instanceof Error ? failure : new Error("email render failed", { cause: failure });
|
|
42
|
+
return html.replace(/^<!DOCTYPE html>/i, "").replace(/<link rel="preload" as="image"[^>]*\/>/g, "");
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { registryRenderer };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=react-email.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-email.mjs","names":[],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type { EmailRenderer } from './cloudflare';\nimport { type EmailTemplate, parseTemplateProps } from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry maps keys to `emailTemplate({...})` definitions. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element, so hooks and context work. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(emails: Record<string, EmailTemplate>): EmailRenderer {\n return async (key, props) => {\n if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);\n const template = emails[key];\n const parsed = parseTemplateProps(template, key, props);\n const element = createElement(template.component as ComponentType<object>, parsed);\n return { subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,MAAM,UACJ;AAEF,SAAgB,iBAAiB,QAAsD;CACrF,OAAO,OAAO,KAAK,UAAU;EAC3B,IAAI,CAAC,OAAO,OAAO,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,2BAA2B,KAAK;EACjF,MAAM,WAAW,OAAO;EACxB,MAAM,SAAS,mBAAmB,UAAU,KAAK,KAAK;EACtD,MAAM,UAAU,cAAc,SAAS,WAAoC,MAAM;EACjF,OAAO;GAAE,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CACpF;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,MAAM,uBAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/reporting/classify.ts
|
|
3
|
+
/** How close together clicks on different links of one message must be to count as one walk. */
|
|
4
|
+
const BURST_WINDOW_MS = 5e3;
|
|
5
|
+
const DEFAULT_QUICK_MS = 1e3;
|
|
6
|
+
const SCANNER = /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\/|wget\/|python|go-http-client|java\/|okhttp/i;
|
|
7
|
+
const DATACENTER = /\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\b/i;
|
|
8
|
+
const GOOGLE_PROXY = /GoogleImageProxy|ggpht\.com/i;
|
|
9
|
+
const human = {
|
|
10
|
+
machine: false,
|
|
11
|
+
reason: null
|
|
12
|
+
};
|
|
13
|
+
const machine = (reason) => ({
|
|
14
|
+
machine: true,
|
|
15
|
+
reason
|
|
16
|
+
});
|
|
17
|
+
const scanner = (facts) => SCANNER.test(facts.userAgent?.trim() ?? "");
|
|
18
|
+
function classifyOpen(facts, options = {}) {
|
|
19
|
+
const quickMs = options.quickMs ?? 1e3;
|
|
20
|
+
const agent = facts.userAgent?.trim() ?? "";
|
|
21
|
+
if (agent === "Mozilla/5.0" || /apple/i.test(facts.organization ?? "")) return machine("apple_privacy_proxy");
|
|
22
|
+
if (GOOGLE_PROXY.test(agent)) return facts.sinceDeliveredMs < quickMs ? machine("gmail_prefetch") : human;
|
|
23
|
+
return scanner(facts) ? machine("scanner") : human;
|
|
24
|
+
}
|
|
25
|
+
function classifyClick(facts, options = {}) {
|
|
26
|
+
const quickMs = options.quickMs ?? 1e3;
|
|
27
|
+
if (facts.method === "HEAD") return machine("head_request");
|
|
28
|
+
if (scanner(facts)) return machine("scanner");
|
|
29
|
+
if (DATACENTER.test(facts.organization ?? "")) return machine("datacenter");
|
|
30
|
+
if (facts.linksInBurst >= 3 || facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount) return machine("link_burst");
|
|
31
|
+
const quick = facts.sinceDeliveredMs < quickMs;
|
|
32
|
+
const pair = facts.linksInBurst >= 2;
|
|
33
|
+
if (quick && pair) return machine("quick+link_pair");
|
|
34
|
+
return human;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
exports.BURST_WINDOW_MS = BURST_WINDOW_MS;
|
|
38
|
+
exports.DEFAULT_QUICK_MS = DEFAULT_QUICK_MS;
|
|
39
|
+
exports.classifyClick = classifyClick;
|
|
40
|
+
exports.classifyOpen = classifyOpen;
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=classify.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.cjs","names":[],"sources":["../../src/reporting/classify.ts"],"sourcesContent":["/**\n * Who opened or clicked: a person, or a machine acting on the message without one — the split\n * between human and machine engagement that open and click rates are read through. No single\n * signal decides on its own unless it is strong; weak ones only count together, the way the\n * ESPs that publish their approach describe it (a quick click alone is a person on a phone as\n * often as a scanner). Every verdict names the rule that made it, and the request's user agent and\n * network are stored beside it, so the rules can be revisited against history.\n *\n * Strong, alone enough:\n * - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,\n * Proofpoint, Safe Links, curl, a headless browser…);\n * - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,\n * Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates\n * links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not\n * for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when\n * the person opens the message;\n * - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on\n * delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;\n * - `head_request` (clicks): a person's click is a GET;\n * - `link_burst` (clicks): three or more different links of one message within a few seconds, or\n * every link of it — the way a scanner walks a message. Earlier clicks of the burst are\n * reclassified too (see `recordClick`).\n *\n * Weak, machine only together:\n * - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);\n * - `link_pair`: another link of the same message clicked within a few seconds.\n *\n * Gmail's image proxy fetches when the person opens the message, so its opens count as human —\n * unless it fetched within `quickMs` of delivery (`gmail_prefetch`).\n */\n\nexport interface Verdict {\n machine: boolean;\n reason: string | null;\n}\n\nexport interface RequestFacts {\n method: string;\n userAgent: string | null;\n /** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */\n organization: string | null;\n /** Milliseconds between delivering the message and this request. */\n sinceDeliveredMs: number;\n}\n\nexport interface ClassifyOptions {\n /** A request this soon after delivery is a weak machine signal; 1000 ms by default. */\n quickMs?: number;\n}\n\nexport interface ClickFacts extends RequestFacts {\n /** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */\n linksInBurst: number;\n /** How many tracked links the message has. */\n linkCount: number;\n}\n\n/** How close together clicks on different links of one message must be to count as one walk. */\nexport const BURST_WINDOW_MS = 5_000;\nexport const DEFAULT_QUICK_MS = 1_000;\n\n// Whole words only: \"San Francisco\" is not Cisco, a CUBOT phone is not a bot\nconst SCANNER =\n /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\\/|wget\\/|python|go-http-client|java\\/|okhttp/i;\nconst DATACENTER =\n /\\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\\b/i;\nconst GOOGLE_PROXY = /GoogleImageProxy|ggpht\\.com/i;\n\nconst human: Verdict = { machine: false, reason: null };\nconst machine = (reason: string): Verdict => ({ machine: true, reason });\n\nconst scanner = (facts: RequestFacts) => SCANNER.test(facts.userAgent?.trim() ?? '');\n\nexport function classifyOpen(facts: RequestFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n const agent = facts.userAgent?.trim() ?? '';\n if (agent === 'Mozilla/5.0' || /apple/i.test(facts.organization ?? '')) {\n return machine('apple_privacy_proxy');\n }\n if (GOOGLE_PROXY.test(agent)) {\n return facts.sinceDeliveredMs < quickMs ? machine('gmail_prefetch') : human;\n }\n return scanner(facts) ? machine('scanner') : human;\n}\n\nexport function classifyClick(facts: ClickFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n if (facts.method === 'HEAD') return machine('head_request');\n if (scanner(facts)) return machine('scanner');\n if (DATACENTER.test(facts.organization ?? '')) return machine('datacenter');\n if (facts.linksInBurst >= 3 || (facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount)) {\n return machine('link_burst');\n }\n const quick = facts.sinceDeliveredMs < quickMs;\n const pair = facts.linksInBurst >= 2;\n if (quick && pair) return machine('quick+link_pair');\n return human;\n}\n"],"mappings":";;;AA0DA,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAGhC,MAAM,UACJ;AACF,MAAM,aACJ;AACF,MAAM,eAAe;AAErB,MAAM,QAAiB;CAAE,SAAS;CAAO,QAAQ;AAAK;AACtD,MAAM,WAAW,YAA6B;CAAE,SAAS;CAAM;AAAO;AAEtE,MAAM,WAAW,UAAwB,QAAQ,KAAK,MAAM,WAAW,KAAK,KAAK,EAAE;AAEnF,SAAgB,aAAa,OAAqB,UAA2B,CAAC,GAAY;CACxF,MAAM,UAAU,QAAQ,WAAA;CACxB,MAAM,QAAQ,MAAM,WAAW,KAAK,KAAK;CACzC,IAAI,UAAU,iBAAiB,SAAS,KAAK,MAAM,gBAAgB,EAAE,GACnE,OAAO,QAAQ,qBAAqB;CAEtC,IAAI,aAAa,KAAK,KAAK,GACzB,OAAO,MAAM,mBAAmB,UAAU,QAAQ,gBAAgB,IAAI;CAExE,OAAO,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI;AAC/C;AAEA,SAAgB,cAAc,OAAmB,UAA2B,CAAC,GAAY;CACvF,MAAM,UAAU,QAAQ,WAAA;CACxB,IAAI,MAAM,WAAW,QAAQ,OAAO,QAAQ,cAAc;CAC1D,IAAI,QAAQ,KAAK,GAAG,OAAO,QAAQ,SAAS;CAC5C,IAAI,WAAW,KAAK,MAAM,gBAAgB,EAAE,GAAG,OAAO,QAAQ,YAAY;CAC1E,IAAI,MAAM,gBAAgB,KAAM,MAAM,aAAa,KAAK,MAAM,gBAAgB,MAAM,WAClF,OAAO,QAAQ,YAAY;CAE7B,MAAM,QAAQ,MAAM,mBAAmB;CACvC,MAAM,OAAO,MAAM,gBAAgB;CACnC,IAAI,SAAS,MAAM,OAAO,QAAQ,iBAAiB;CACnD,OAAO;AACT"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/reporting/classify.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Who opened or clicked: a person, or a machine acting on the message without one — the split
|
|
4
|
+
* between human and machine engagement that open and click rates are read through. No single
|
|
5
|
+
* signal decides on its own unless it is strong; weak ones only count together, the way the
|
|
6
|
+
* ESPs that publish their approach describe it (a quick click alone is a person on a phone as
|
|
7
|
+
* often as a scanner). Every verdict names the rule that made it, and the request's user agent and
|
|
8
|
+
* network are stored beside it, so the rules can be revisited against history.
|
|
9
|
+
*
|
|
10
|
+
* Strong, alone enough:
|
|
11
|
+
* - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,
|
|
12
|
+
* Proofpoint, Safe Links, curl, a headless browser…);
|
|
13
|
+
* - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,
|
|
14
|
+
* Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates
|
|
15
|
+
* links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not
|
|
16
|
+
* for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when
|
|
17
|
+
* the person opens the message;
|
|
18
|
+
* - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on
|
|
19
|
+
* delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;
|
|
20
|
+
* - `head_request` (clicks): a person's click is a GET;
|
|
21
|
+
* - `link_burst` (clicks): three or more different links of one message within a few seconds, or
|
|
22
|
+
* every link of it — the way a scanner walks a message. Earlier clicks of the burst are
|
|
23
|
+
* reclassified too (see `recordClick`).
|
|
24
|
+
*
|
|
25
|
+
* Weak, machine only together:
|
|
26
|
+
* - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);
|
|
27
|
+
* - `link_pair`: another link of the same message clicked within a few seconds.
|
|
28
|
+
*
|
|
29
|
+
* Gmail's image proxy fetches when the person opens the message, so its opens count as human —
|
|
30
|
+
* unless it fetched within `quickMs` of delivery (`gmail_prefetch`).
|
|
31
|
+
*/
|
|
32
|
+
export interface Verdict {
|
|
33
|
+
machine: boolean;
|
|
34
|
+
reason: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface RequestFacts {
|
|
37
|
+
method: string;
|
|
38
|
+
userAgent: string | null;
|
|
39
|
+
/** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */
|
|
40
|
+
organization: string | null;
|
|
41
|
+
/** Milliseconds between delivering the message and this request. */
|
|
42
|
+
sinceDeliveredMs: number;
|
|
43
|
+
}
|
|
44
|
+
export interface ClassifyOptions {
|
|
45
|
+
/** A request this soon after delivery is a weak machine signal; 1000 ms by default. */
|
|
46
|
+
quickMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ClickFacts extends RequestFacts {
|
|
49
|
+
/** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */
|
|
50
|
+
linksInBurst: number;
|
|
51
|
+
/** How many tracked links the message has. */
|
|
52
|
+
linkCount: number;
|
|
53
|
+
}
|
|
54
|
+
/** How close together clicks on different links of one message must be to count as one walk. */
|
|
55
|
+
export declare const BURST_WINDOW_MS = 5000;
|
|
56
|
+
export declare const DEFAULT_QUICK_MS = 1000;
|
|
57
|
+
export declare function classifyOpen(facts: RequestFacts, options?: ClassifyOptions): Verdict;
|
|
58
|
+
export declare function classifyClick(facts: ClickFacts, options?: ClassifyOptions): Verdict;
|
|
59
|
+
//#endregion
|
|
60
|
+
//# sourceMappingURL=classify.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.cts","names":[],"sources":["../../src/reporting/classify.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BiB;EACf;EACA;;iBAGe;EACf;EACA;;EAEA;;EAEA;;iBAGe;;EAEf;;iBAGe,mBAAmB;;EAElC;;EAEA;;;qBAIW;qBACA;wBAcG,aAAa,OAAO,cAAc,UAAS,kBAAuB;wBAYlE,cAAc,OAAO,YAAY,UAAS,kBAAuB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/reporting/classify.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Who opened or clicked: a person, or a machine acting on the message without one — the split
|
|
4
|
+
* between human and machine engagement that open and click rates are read through. No single
|
|
5
|
+
* signal decides on its own unless it is strong; weak ones only count together, the way the
|
|
6
|
+
* ESPs that publish their approach describe it (a quick click alone is a person on a phone as
|
|
7
|
+
* often as a scanner). Every verdict names the rule that made it, and the request's user agent and
|
|
8
|
+
* network are stored beside it, so the rules can be revisited against history.
|
|
9
|
+
*
|
|
10
|
+
* Strong, alone enough:
|
|
11
|
+
* - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,
|
|
12
|
+
* Proofpoint, Safe Links, curl, a headless browser…);
|
|
13
|
+
* - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,
|
|
14
|
+
* Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates
|
|
15
|
+
* links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not
|
|
16
|
+
* for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when
|
|
17
|
+
* the person opens the message;
|
|
18
|
+
* - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on
|
|
19
|
+
* delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;
|
|
20
|
+
* - `head_request` (clicks): a person's click is a GET;
|
|
21
|
+
* - `link_burst` (clicks): three or more different links of one message within a few seconds, or
|
|
22
|
+
* every link of it — the way a scanner walks a message. Earlier clicks of the burst are
|
|
23
|
+
* reclassified too (see `recordClick`).
|
|
24
|
+
*
|
|
25
|
+
* Weak, machine only together:
|
|
26
|
+
* - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);
|
|
27
|
+
* - `link_pair`: another link of the same message clicked within a few seconds.
|
|
28
|
+
*
|
|
29
|
+
* Gmail's image proxy fetches when the person opens the message, so its opens count as human —
|
|
30
|
+
* unless it fetched within `quickMs` of delivery (`gmail_prefetch`).
|
|
31
|
+
*/
|
|
32
|
+
export interface Verdict {
|
|
33
|
+
machine: boolean;
|
|
34
|
+
reason: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface RequestFacts {
|
|
37
|
+
method: string;
|
|
38
|
+
userAgent: string | null;
|
|
39
|
+
/** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */
|
|
40
|
+
organization: string | null;
|
|
41
|
+
/** Milliseconds between delivering the message and this request. */
|
|
42
|
+
sinceDeliveredMs: number;
|
|
43
|
+
}
|
|
44
|
+
export interface ClassifyOptions {
|
|
45
|
+
/** A request this soon after delivery is a weak machine signal; 1000 ms by default. */
|
|
46
|
+
quickMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ClickFacts extends RequestFacts {
|
|
49
|
+
/** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */
|
|
50
|
+
linksInBurst: number;
|
|
51
|
+
/** How many tracked links the message has. */
|
|
52
|
+
linkCount: number;
|
|
53
|
+
}
|
|
54
|
+
/** How close together clicks on different links of one message must be to count as one walk. */
|
|
55
|
+
export declare const BURST_WINDOW_MS = 5000;
|
|
56
|
+
export declare const DEFAULT_QUICK_MS = 1000;
|
|
57
|
+
export declare function classifyOpen(facts: RequestFacts, options?: ClassifyOptions): Verdict;
|
|
58
|
+
export declare function classifyClick(facts: ClickFacts, options?: ClassifyOptions): Verdict;
|
|
59
|
+
//#endregion
|
|
60
|
+
//# sourceMappingURL=classify.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.mts","names":[],"sources":["../../src/reporting/classify.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BiB;EACf;EACA;;iBAGe;EACf;EACA;;EAEA;;EAEA;;iBAGe;;EAEf;;iBAGe,mBAAmB;;EAElC;;EAEA;;;qBAIW;qBACA;wBAcG,aAAa,OAAO,cAAc,UAAS,kBAAuB;wBAYlE,cAAc,OAAO,YAAY,UAAS,kBAAuB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/reporting/classify.ts
|
|
2
|
+
/** How close together clicks on different links of one message must be to count as one walk. */
|
|
3
|
+
const BURST_WINDOW_MS = 5e3;
|
|
4
|
+
const DEFAULT_QUICK_MS = 1e3;
|
|
5
|
+
const SCANNER = /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\/|wget\/|python|go-http-client|java\/|okhttp/i;
|
|
6
|
+
const DATACENTER = /\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\b/i;
|
|
7
|
+
const GOOGLE_PROXY = /GoogleImageProxy|ggpht\.com/i;
|
|
8
|
+
const human = {
|
|
9
|
+
machine: false,
|
|
10
|
+
reason: null
|
|
11
|
+
};
|
|
12
|
+
const machine = (reason) => ({
|
|
13
|
+
machine: true,
|
|
14
|
+
reason
|
|
15
|
+
});
|
|
16
|
+
const scanner = (facts) => SCANNER.test(facts.userAgent?.trim() ?? "");
|
|
17
|
+
function classifyOpen(facts, options = {}) {
|
|
18
|
+
const quickMs = options.quickMs ?? 1e3;
|
|
19
|
+
const agent = facts.userAgent?.trim() ?? "";
|
|
20
|
+
if (agent === "Mozilla/5.0" || /apple/i.test(facts.organization ?? "")) return machine("apple_privacy_proxy");
|
|
21
|
+
if (GOOGLE_PROXY.test(agent)) return facts.sinceDeliveredMs < quickMs ? machine("gmail_prefetch") : human;
|
|
22
|
+
return scanner(facts) ? machine("scanner") : human;
|
|
23
|
+
}
|
|
24
|
+
function classifyClick(facts, options = {}) {
|
|
25
|
+
const quickMs = options.quickMs ?? 1e3;
|
|
26
|
+
if (facts.method === "HEAD") return machine("head_request");
|
|
27
|
+
if (scanner(facts)) return machine("scanner");
|
|
28
|
+
if (DATACENTER.test(facts.organization ?? "")) return machine("datacenter");
|
|
29
|
+
if (facts.linksInBurst >= 3 || facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount) return machine("link_burst");
|
|
30
|
+
const quick = facts.sinceDeliveredMs < quickMs;
|
|
31
|
+
const pair = facts.linksInBurst >= 2;
|
|
32
|
+
if (quick && pair) return machine("quick+link_pair");
|
|
33
|
+
return human;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { BURST_WINDOW_MS, DEFAULT_QUICK_MS, classifyClick, classifyOpen };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=classify.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.mjs","names":[],"sources":["../../src/reporting/classify.ts"],"sourcesContent":["/**\n * Who opened or clicked: a person, or a machine acting on the message without one — the split\n * between human and machine engagement that open and click rates are read through. No single\n * signal decides on its own unless it is strong; weak ones only count together, the way the\n * ESPs that publish their approach describe it (a quick click alone is a person on a phone as\n * often as a scanner). Every verdict names the rule that made it, and the request's user agent and\n * network are stored beside it, so the rules can be revisited against history.\n *\n * Strong, alone enough:\n * - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,\n * Proofpoint, Safe Links, curl, a headless browser…);\n * - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,\n * Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates\n * links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not\n * for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when\n * the person opens the message;\n * - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on\n * delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;\n * - `head_request` (clicks): a person's click is a GET;\n * - `link_burst` (clicks): three or more different links of one message within a few seconds, or\n * every link of it — the way a scanner walks a message. Earlier clicks of the burst are\n * reclassified too (see `recordClick`).\n *\n * Weak, machine only together:\n * - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);\n * - `link_pair`: another link of the same message clicked within a few seconds.\n *\n * Gmail's image proxy fetches when the person opens the message, so its opens count as human —\n * unless it fetched within `quickMs` of delivery (`gmail_prefetch`).\n */\n\nexport interface Verdict {\n machine: boolean;\n reason: string | null;\n}\n\nexport interface RequestFacts {\n method: string;\n userAgent: string | null;\n /** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */\n organization: string | null;\n /** Milliseconds between delivering the message and this request. */\n sinceDeliveredMs: number;\n}\n\nexport interface ClassifyOptions {\n /** A request this soon after delivery is a weak machine signal; 1000 ms by default. */\n quickMs?: number;\n}\n\nexport interface ClickFacts extends RequestFacts {\n /** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */\n linksInBurst: number;\n /** How many tracked links the message has. */\n linkCount: number;\n}\n\n/** How close together clicks on different links of one message must be to count as one walk. */\nexport const BURST_WINDOW_MS = 5_000;\nexport const DEFAULT_QUICK_MS = 1_000;\n\n// Whole words only: \"San Francisco\" is not Cisco, a CUBOT phone is not a bot\nconst SCANNER =\n /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\\/|wget\\/|python|go-http-client|java\\/|okhttp/i;\nconst DATACENTER =\n /\\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\\b/i;\nconst GOOGLE_PROXY = /GoogleImageProxy|ggpht\\.com/i;\n\nconst human: Verdict = { machine: false, reason: null };\nconst machine = (reason: string): Verdict => ({ machine: true, reason });\n\nconst scanner = (facts: RequestFacts) => SCANNER.test(facts.userAgent?.trim() ?? '');\n\nexport function classifyOpen(facts: RequestFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n const agent = facts.userAgent?.trim() ?? '';\n if (agent === 'Mozilla/5.0' || /apple/i.test(facts.organization ?? '')) {\n return machine('apple_privacy_proxy');\n }\n if (GOOGLE_PROXY.test(agent)) {\n return facts.sinceDeliveredMs < quickMs ? machine('gmail_prefetch') : human;\n }\n return scanner(facts) ? machine('scanner') : human;\n}\n\nexport function classifyClick(facts: ClickFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n if (facts.method === 'HEAD') return machine('head_request');\n if (scanner(facts)) return machine('scanner');\n if (DATACENTER.test(facts.organization ?? '')) return machine('datacenter');\n if (facts.linksInBurst >= 3 || (facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount)) {\n return machine('link_burst');\n }\n const quick = facts.sinceDeliveredMs < quickMs;\n const pair = facts.linksInBurst >= 2;\n if (quick && pair) return machine('quick+link_pair');\n return human;\n}\n"],"mappings":";;AA0DA,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAGhC,MAAM,UACJ;AACF,MAAM,aACJ;AACF,MAAM,eAAe;AAErB,MAAM,QAAiB;CAAE,SAAS;CAAO,QAAQ;AAAK;AACtD,MAAM,WAAW,YAA6B;CAAE,SAAS;CAAM;AAAO;AAEtE,MAAM,WAAW,UAAwB,QAAQ,KAAK,MAAM,WAAW,KAAK,KAAK,EAAE;AAEnF,SAAgB,aAAa,OAAqB,UAA2B,CAAC,GAAY;CACxF,MAAM,UAAU,QAAQ,WAAA;CACxB,MAAM,QAAQ,MAAM,WAAW,KAAK,KAAK;CACzC,IAAI,UAAU,iBAAiB,SAAS,KAAK,MAAM,gBAAgB,EAAE,GACnE,OAAO,QAAQ,qBAAqB;CAEtC,IAAI,aAAa,KAAK,KAAK,GACzB,OAAO,MAAM,mBAAmB,UAAU,QAAQ,gBAAgB,IAAI;CAExE,OAAO,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI;AAC/C;AAEA,SAAgB,cAAc,OAAmB,UAA2B,CAAC,GAAY;CACvF,MAAM,UAAU,QAAQ,WAAA;CACxB,IAAI,MAAM,WAAW,QAAQ,OAAO,QAAQ,cAAc;CAC1D,IAAI,QAAQ,KAAK,GAAG,OAAO,QAAQ,SAAS;CAC5C,IAAI,WAAW,KAAK,MAAM,gBAAgB,EAAE,GAAG,OAAO,QAAQ,YAAY;CAC1E,IAAI,MAAM,gBAAgB,KAAM,MAAM,aAAa,KAAK,MAAM,gBAAgB,MAAM,WAClF,OAAO,QAAQ,YAAY;CAE7B,MAAM,QAAQ,MAAM,mBAAmB;CACvC,MAAM,OAAO,MAAM,gBAAgB;CACnC,IAAI,SAAS,MAAM,OAAO,QAAQ,iBAAiB;CACnD,OAAO;AACT"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/reporting/observer.ts
|
|
3
|
+
/**
|
|
4
|
+
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
5
|
+
* and outcome recorded through `store.reporting`.
|
|
6
|
+
*
|
|
7
|
+
* A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
|
|
8
|
+
* an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
|
|
9
|
+
* `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
|
|
10
|
+
* `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
|
|
11
|
+
* plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
|
|
12
|
+
* The links are recorded when the sender commits the rewritten email, before it goes out.
|
|
13
|
+
*/
|
|
14
|
+
function reportingObserver(reporting, origin) {
|
|
15
|
+
const base = origin?.replace(/\/$/, "");
|
|
16
|
+
return {
|
|
17
|
+
started: (event) => reporting.instanceStarted(event),
|
|
18
|
+
nodeReached: (event) => reporting.nodeReached(event),
|
|
19
|
+
async messagePrepared(message) {
|
|
20
|
+
const id = await reporting.recordMessage({
|
|
21
|
+
idempotencyKey: message.idempotencyKey,
|
|
22
|
+
workflow: message.workflow,
|
|
23
|
+
instanceId: message.instanceId,
|
|
24
|
+
nodeId: message.nodeId,
|
|
25
|
+
template: message.template,
|
|
26
|
+
channel: channelOf(message),
|
|
27
|
+
userId: message.userId,
|
|
28
|
+
sentAt: Date.now()
|
|
29
|
+
});
|
|
30
|
+
if (message.channel === "email" && base !== void 0) {
|
|
31
|
+
const urls = [];
|
|
32
|
+
return {
|
|
33
|
+
link: (url) => {
|
|
34
|
+
urls.push(url);
|
|
35
|
+
return `${base}/t/c/${id}/${urls.length - 1}`;
|
|
36
|
+
},
|
|
37
|
+
pixel: `${base}/t/o/${id}`,
|
|
38
|
+
commit: () => reporting.recordLinks(id, urls)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (message.channel === "push") return { data: {
|
|
42
|
+
message_id: id,
|
|
43
|
+
...base === void 0 ? {} : {
|
|
44
|
+
delivered_url: `${base}/t/d/${id}`,
|
|
45
|
+
opened_url: `${base}/t/p/${id}`
|
|
46
|
+
}
|
|
47
|
+
} };
|
|
48
|
+
},
|
|
49
|
+
async messageSent(message, result) {
|
|
50
|
+
await reporting.settleMessage(message.idempotencyKey, result, Date.now());
|
|
51
|
+
},
|
|
52
|
+
finished: (event) => reporting.instanceFinished(event)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */
|
|
56
|
+
function channelOf(message) {
|
|
57
|
+
if (message.channel !== "push") return message.channel;
|
|
58
|
+
return `push_${message.device?.platform ?? "unknown"}`;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
exports.reportingObserver = reportingObserver;
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=observer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.cjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel\n * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n link: (url) => {\n urls.push(url);\n return `${base}/t/c/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,OAAO,QAAQ;MACb,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,SAAS;KAC5C;KACA,OAAO,GAAG,KAAK,OAAO;KAEtB,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JourneyObserver } from "../engine/ports.cjs";
|
|
2
|
+
import "../engine/index.cjs";
|
|
3
|
+
import { JourneyReporting } from "../store/index.cjs";
|
|
4
|
+
//#region src/reporting/observer.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
7
|
+
* and outcome recorded through `store.reporting`.
|
|
8
|
+
*
|
|
9
|
+
* A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
|
|
10
|
+
* an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
|
|
11
|
+
* `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
|
|
12
|
+
* `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
|
|
13
|
+
* plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
|
|
14
|
+
* The links are recorded when the sender commits the rewritten email, before it goes out.
|
|
15
|
+
*/
|
|
16
|
+
export declare function reportingObserver(reporting: JourneyReporting, origin: string | undefined): JourneyObserver;
|
|
17
|
+
//#endregion
|
|
18
|
+
//# sourceMappingURL=observer.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.d.cts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":";;;;;;;;;;;;;;;wBAcgB,kBACd,WAAW,kBACX,6BACC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JourneyObserver } from "../engine/ports.mjs";
|
|
2
|
+
import "../engine/index.mjs";
|
|
3
|
+
import { JourneyReporting } from "../store/index.mjs";
|
|
4
|
+
//#region src/reporting/observer.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
7
|
+
* and outcome recorded through `store.reporting`.
|
|
8
|
+
*
|
|
9
|
+
* A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
|
|
10
|
+
* an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
|
|
11
|
+
* `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
|
|
12
|
+
* `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
|
|
13
|
+
* plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
|
|
14
|
+
* The links are recorded when the sender commits the rewritten email, before it goes out.
|
|
15
|
+
*/
|
|
16
|
+
export declare function reportingObserver(reporting: JourneyReporting, origin: string | undefined): JourneyObserver;
|
|
17
|
+
//#endregion
|
|
18
|
+
//# sourceMappingURL=observer.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.d.mts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":";;;;;;;;;;;;;;;wBAcgB,kBACd,WAAW,kBACX,6BACC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/reporting/observer.ts
|
|
2
|
+
/**
|
|
3
|
+
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
4
|
+
* and outcome recorded through `store.reporting`.
|
|
5
|
+
*
|
|
6
|
+
* A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
|
|
7
|
+
* an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
|
|
8
|
+
* `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
|
|
9
|
+
* `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
|
|
10
|
+
* plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
|
|
11
|
+
* The links are recorded when the sender commits the rewritten email, before it goes out.
|
|
12
|
+
*/
|
|
13
|
+
function reportingObserver(reporting, origin) {
|
|
14
|
+
const base = origin?.replace(/\/$/, "");
|
|
15
|
+
return {
|
|
16
|
+
started: (event) => reporting.instanceStarted(event),
|
|
17
|
+
nodeReached: (event) => reporting.nodeReached(event),
|
|
18
|
+
async messagePrepared(message) {
|
|
19
|
+
const id = await reporting.recordMessage({
|
|
20
|
+
idempotencyKey: message.idempotencyKey,
|
|
21
|
+
workflow: message.workflow,
|
|
22
|
+
instanceId: message.instanceId,
|
|
23
|
+
nodeId: message.nodeId,
|
|
24
|
+
template: message.template,
|
|
25
|
+
channel: channelOf(message),
|
|
26
|
+
userId: message.userId,
|
|
27
|
+
sentAt: Date.now()
|
|
28
|
+
});
|
|
29
|
+
if (message.channel === "email" && base !== void 0) {
|
|
30
|
+
const urls = [];
|
|
31
|
+
return {
|
|
32
|
+
link: (url) => {
|
|
33
|
+
urls.push(url);
|
|
34
|
+
return `${base}/t/c/${id}/${urls.length - 1}`;
|
|
35
|
+
},
|
|
36
|
+
pixel: `${base}/t/o/${id}`,
|
|
37
|
+
commit: () => reporting.recordLinks(id, urls)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (message.channel === "push") return { data: {
|
|
41
|
+
message_id: id,
|
|
42
|
+
...base === void 0 ? {} : {
|
|
43
|
+
delivered_url: `${base}/t/d/${id}`,
|
|
44
|
+
opened_url: `${base}/t/p/${id}`
|
|
45
|
+
}
|
|
46
|
+
} };
|
|
47
|
+
},
|
|
48
|
+
async messageSent(message, result) {
|
|
49
|
+
await reporting.settleMessage(message.idempotencyKey, result, Date.now());
|
|
50
|
+
},
|
|
51
|
+
finished: (event) => reporting.instanceFinished(event)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */
|
|
55
|
+
function channelOf(message) {
|
|
56
|
+
if (message.channel !== "push") return message.channel;
|
|
57
|
+
return `push_${message.device?.platform ?? "unknown"}`;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { reportingObserver };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=observer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observer.mjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel\n * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n link: (url) => {\n urls.push(url);\n return `${base}/t/c/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,OAAO,QAAQ;MACb,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,SAAS;KAC5C;KACA,OAAO,GAAG,KAAK,OAAO;KAEtB,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
|